什麼是物件導向
從閉包那邊的程式碼來看
function getWallet() {
var my_balance = 999
return {
deduct: function(n) {
my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
}
}
}
var wallet = getWallet()
wallet.deduct(13) // 只被扣 10 塊
my_balance -= 999 // Uncaught ReferenceError: my_balance is not defined
其中 wallet.deduct(13)
表示對 wallet 這個物件呼叫裡面的 function,這差不多是物件導向的概念,==也就是要對某個物件作操作==。
而一般直接呼叫 function 的寫法是這樣 deduct(wallet)
class 範例實作物件導向(ES6)
class Dog {
constructor(name) {
this.name = name
// this 指到下面的 instance 所呼叫的不同變數
}
getName() {
return this.name
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog('abc') // 這叫 instance
// new -> 就是呼叫 class Dog 內的 constructor 建構子
// 因為 d 呼叫了 class Dog 內的 constructor
// 所以上面 constructor 的 this 就會指到 d 這個變數
d.sayHello() // abc
var b = new Dog('blabla')
b.sayHello() // blabla
ES5 的 class
// constructor
function Person(name, age) {
this.name = name;
this.age = age;
}
var nick = new Person('nick', 18); // instance
var peter = new Person('peter', 18); // instance
Person
是一個構造函數,可以用new這個關鍵字 new 出一個 instance 來。
除此之外,也可以幫Person
加入一些方法。
function Person(name, age) {
this.name = name;
this.age = age;
this.log = function () {
console.log(this.name + ', age:' + this.age);
}
}
var nick = new Person('nick', 18);
nick.log(); // nick, age:18
var peter = new Person('peter', 20);
peter.log(); // peter, age:20
可是這樣其實還有一個小問題, name 跟 age 這兩個屬性,很明顯是每一個 instance 都會不一樣的。可是 log 這個 method,其實是每一個 instance 彼此之間可以共享的,因為都在做同一件事情。
在現在這種情況下,雖然 nick 的 log 這個 function 跟 peter 的 log 這個 function 是在做同一件事,但其實還是佔用了兩份空間,意思就是他們其實是兩個不同的 function。
那怎麼辦呢?我們可以把這個 function 抽出來,變成所有 Person 都可以共享的方法。講到這邊,你應該有聽過一個東西叫做 prototype。只要把 log 這個 function 指定在 Person.prototype 上面,所有 Person 的 instance 都可以共享這個方法。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
var peter = new Person('peter', 20);
console.log(nick.log === peter.log) // true
// 功能依舊跟之前一樣
nick.log(); // nick, age:18
peter.log(); // peter, age:20
小總結
你有一個叫做Person
的函數,就可以把Person
當作 constructor,利用var obj = new Person()
來 new 出一個Person
的 instance,並且可以在Person.prototype
上面加上你想讓所有 instance 共享的屬性或是方法。
教學影片範例
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
Dog.prototype.sayHello = function() {
console.log(this.name)
}
var d = new Dog('abc')
// 使用 new 的 function(這裡指Dog()) 就會被
// 當成 constructor 使用
d.sayHello()
// 可以呼叫上面的 Dog.prototype.sayHello
var b = new Dog('blabla')
b.sayHello()
探究原型鍊原理
以上面var nick = new Person('nick', 18);
的例子來說,當我在呼叫nick.log()
的時候,JavaScript 是怎麼找到這個 function 的?
因為 nick 這個 instance 本身並沒有 log 這個 function。==但根據 JavaScript 的機制,nick 是 Person 的 instance,所以如果在 nick 本身找不到 log,它會試著從Person.prototype
去找。==
==nick 跟Person.prototype
會透過__proto__
的方式連接起來,才知道說要往哪邊去找 log 這個 function。==
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
console.log(nick.__proto__ === Person.prototype) // true
nick 的__proto__
會指向Person.prototype
,所以在發現 nick 沒有 log 這個 method 的時候,==JavaScript 就會試著透過__proto__
找到Person.prototype
==,去看Person.prototype
裡面有沒有 log 這個 method。
假如Person.prototype
還是沒有,就繼續依照這個規則,去看Person.prototype.__proto__
裡面有沒有 log 這個 method,就這樣一直不斷找下去。找到某個東西的__proto__
是 null 為止。意思就是這邊是最上層了。
==而上面這一條透過__proto__
不斷串起來的鍊,就叫做原型鍊==。透過這一條原型鍊,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。
其中因為Person
其實就是個 Function 的 instance,所以Person.__proto__
就是Function.prototype
。
底下是原型鍊的解釋 code
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
// 這個剛講過了,nick.__proto__ 會指向 Person.prototype
console.log(nick.__proto__ === Person.prototype) // true
// 那 Person.prototype.__proto__ 會指向誰呢?會指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
// 那 Object.prototype.__proto__ 又會指向誰呢?會指向 null,這就是原型鍊的頂端了
console.log(Object.prototype.__proto__) // null
如果想知道一個屬性是存在 instance 身上,還是存在於它屬於的原型鍊當中,可以用hasOwnProperty
這個方法:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
console.log(nick.hasOwnProperty('log')); // false
console.log(nick.__proto__.hasOwnProperty('log')); // true
new 做了什麼
有了原型鍊的概念之後,就不難理解new這個關鍵字背後會做的事情是什麼。
假設現在有一行程式碼是:var nick = new Person('nick');
,那它有以下幾件事情要做:
- 創出一個新的 object,我們叫它 O
- 把 O 的
__proto__
指向Person
的 prototype,才能繼承原型鍊 - 拿 O 當作 context,呼叫
Person
這個建構函式 - 回傳 O
我們可以寫一段程式碼來模擬這個情形:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
function newObj(Constructor, arguments) {
var o = new Object();
// 讓 o 繼承原型鍊
o.__proto__ = Constructor.prototype;
// 執行建構函式
Constructor.apply(o, arguments);
// 回傳建立好的物件
return o;
}
var nick = newObj(Person, ['nick', 18]);
nick.log(); // nick, age:18
new 的教學影片範例:
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
Dog.prototype.sayHello = function() {
console.log(this.name)
}
//var d = new Dog('hi')
var b = newDog('hello')
b.sayHello()
// newDog() 要做 new 做的事
function newDog(name) {
var obj = {} // 建立新的 obj
Dog.call(obj, name)
// 呼叫任意 constructor,這邊呼叫 Dog
// 把 obj 當作 constructor 的 this 丟進去
// 所以 Dog 內的 this 就是這邊傳入的 obj
console.log(obj) // 此時的 obj => { name: 'hello' }
obj.__proto__ = Dog.prototype
// 設定要關聯的 prototype
// 就可以使用上面的 Dog.prototype 相關的 function
return obj
}
小補充:
function test() {
console.log(this)
}
test.call({}) // {}
// 使用 call 呼叫時,
// test 函式內的 this 就會對應到 call() 傳入的參數
還有 instanceof
, constructor
等函式,詳見參考資料
參考資料:該來理解 JavaScript 的原型鍊了