原型鍊 文章推薦:該來理解 JavaScript 的原型鍊了
什麼是物件導向?
ES6 的物件導向(基礎範例)
範例一
class
的名稱一定要大寫開頭- 先用
class
畫出一個設計圖,列出Dog
有哪些 method 可以用 - 再用
new Dog()
從「Dog
這個 class」實際建立出一個物件(instance)
然後才可以使用 d.sayHello()
// 先用 class 畫出一個設計圖
class Dog {
sayHello() {
console.log("hello");
}
}
var d = new Dog(); // 實際建立出一個物件(instance)
d.sayHello(); // hello
this
會指向「目前所在的 instance」
如果 this
是出現在物件導向「裡面」的話:
d.setName("harry")
代表「我要對 d
做 setName("harry")
」,所以這個 this
就會指向「呼叫 setName("harry")
的 instance」,也就是 d
第 4 行的 this.name = name
就是把 d
的 name
設為「我傳入的參數 name
」
setter 和 getter 是很常見的模式
setName(name)
函式稱為 setter,專門用來設定東西用的getName()
函式稱為 getter,專門用來取得值
雖然用 d.name
也可以取得/更改值,但是不建議這樣寫。還是會建議用 class Dog
裡面提供的 method,也就是 d.getName()
和 d.setName()
來取得/更改 d
的 name
的值,這是比較好的開發習慣
// 先用 class 畫出一個設計圖
class Dog {
// setter
setName(name) {
this.name = name; // this 會指向 d
}
// getter
getName() {
return this.name;
}
sayHello() {
console.log("hello");
}
}
var d = new Dog(); // 實際建立出一個物件(instance)
d.setName("harry");
console.log(d.getName()); // harry
用 constructor
函式做初始化
new Dog()
就像是一個 function call,可以傳參數進去,例如用 new Dog("danny")
把狗取名為 danny,用 new Dog("ben")
把狗取名為 ben
然後,在 class Dog
裡面就可以用 constructor
函式來接收參數
constructor
是一個特別的 function,稱為「建構子」,常用來做初始化
當我呼叫 new Dog("danny")
時,其實就是在呼叫 constructor
函式
所以,在 new Dog("danny")
裡面傳入的參數 danny
,就可以在 constructor(name)
這裡的 name
接收到,並且把 this.name
設定為 danny
// 先用 class 畫出一個設計圖
class Dog {
// 用 constructor 接收參數
constructor(name) {
this.name = name;
}
// setter
setName(name) {
this.name = name; // this 會指向 d
}
// getter
getName() {
return this.name;
}
sayHello() {
console.log(this.name);
}
}
var d = new Dog("danny"); // 實際建立出一個物件(instance)
d.sayHello(); // danny
var b = new Dog("ben");
b.sayHello(); // ben
console.log(d.sayHello === b.sayHello); // true
從 d.sayHello === b.sayHello
是 true 可以得知「d.sayHello
和 b.sayHello
是同一個 function」,只是會根據 this
指向不同的 instance,而印出不同的 this.name
ES5 的 class
因為在 ES5 裡面,沒有 class
這個語法可以用,所以要用其他方式來實作出物件導向:
// ES5 的物件導向
function Dog(name) {
let myName = name;
return {
getName: function () {
return myName;
},
sayHello: function () {
console.log(myName);
},
};
}
let d = Dog("danny");
d.sayHello(); // danny
let b = Dog("ben");
b.sayHello(); // ben
但是,這樣寫會有個問題是:
每呼叫一次 Dog
函式,都會產生一個新的物件,重新產生並回傳 getName
和 sayHello
這兩個 function
從 d.getName === b.getName
是 false 可以得知「d.getName
和 b.getName
是不同的兩個 function」
那如果我有 1000 隻狗,豈不是就會有 1000 個 getName
函式?這樣會很耗費記憶體
所以,應該要可以共用 getName
函式才對,因為都是要做同樣一件事情(就是要 get name 而已),其實只需要一個 getName
函式就夠了!
// ES5 的物件導向
function Dog(name) {
let myName = name;
return {
getName: function () {
return myName;
},
sayHello: function () {
console.log(myName);
},
};
}
let d = Dog("danny");
let b = Dog("ben");
console.log(d.getName === b.getName); // false
要怎麼解決上面的問題呢?解法如下:
在 ES5,可以把 function 當作 constructor
來用,來實作出物件導向
只要我在呼叫 Dog("danny")
前面加上 new
這個關鍵字,JavaScript 就會幫我在背後做好這整個機制:把 Dog
函式,變成是 ES6 的 constructor
函式的意思
這樣一來,d
就會是一個 instance 了
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
var d = new Dog("danny"); // 加上 new 這個關鍵字
console.log(d); // Dog { name: 'danny' }
用 prototype
在 Dog
裡面建立 method
prototype
是 JS 的一個機制
用 Dog.prototype.sayHello
就可以幫 Dog
的 prototype
加上 sayHello
這個 function 了
然後,用 d.sayHello()
就可以呼叫到 Dog.prototype
裡面的 sayHello
這個 function
這時,d.sayHello
和 b.sayHello
會是同一個 function,因為這兩個都是在同一個 prototype
上面
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
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("danny");
d.sayHello(); // danny
var b = new Dog("ben");
b.sayHello(); // ben
console.log(d.sayHello === b.sayHello); // true
從 prototype 來看「原型鍊」
(ES6 的物件導向,在底層就是用 prototype 去實作出來的)
d
是 Dog
的一個 instance,d
和 Dog.prototype
之間,會透過 __proto__
這個內部屬性給連接起來,這樣 JS 的引擎才會知道「呼叫 d.sayHello()
時,就要去 Dog.prototype
裡面找 sayHello
這個 function」
在 JS 有一個內部的屬性叫做 __proto__
,這個屬性會暗示說「如果在 d
身上找不到 sayHello
的話,就要去 __proto__
找」
console.log(d.__proto__)
印出的結果會是:
{ getName: [Function (anonymous)], sayHello: [Function (anonymous)] }
因為 d
是 Dog
的一個 instance,所以 d.__proto__
會等於 Dog.prototype
(這是 new
這個關鍵字幫我設定好的)
console.log(d.__proto__ === Dog.prototype); // true
當我呼叫 d.sayHello()
時,依序會是這樣的流程:
- 去
d
身上找,有沒有sayHello
-> 沒有 - 去
d.__proto__
身上找,有沒有sayHello
因為d.__proto__
=Dog.prototype
,所以就會去Dog.prototype
找到sayHello
並呼叫,裡面的this
就會是d
- 如果還是沒找到
sayHello
的話,就會再往上一層的d.__proto__.__proto__
找,有沒有sayHello
因為d.__proto__.__proto__
=Object.prototype
,所以就會去Object.prototype
找到sayHello
並呼叫,裡面的this
就會是d
- 如果還是沒找到
sayHello
的話,就會再往上一層的d.__proto__.__proto__.__proto__
找,有沒有sayHello
會發現,d.__proto__.__proto__.__proto__
已經回傳null
了,就代表「已經找到頂了,都沒有找到這個 function」,那就會拋出錯誤「d.sayHello is not a function」
d.sayHello()
1. d 身上有沒有 sayHello
2. d.__proto__ 有沒有 sayHello
3. d.__proto__.__proto__ 有沒有 sayHello
4. d.__proto__.__proto__.__proto__ 有沒有 sayHello
5. null 找到頂了
d.__proto__ = Dog.prototype
d.__proto__.__proto__ = Object.prototype
Dog.prototype.__proto__ = Object.prototype
透過 __proto__
這個內部的屬性所構成的「prototype chain 原型鍊」,一層一層往上找,看是否能找到對應的 function
如果 Dog.prototype
和 Object.prototype
同時都有 sayHello
的話
因為會先在 Dog.prototype
找到 sayHello
,就不會再繼續往上找了,所以印出的結果會是 dog danny
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log("dog", this.name);
};
Object.prototype.sayHello = function () {
console.log("object", this.name);
};
var d = new Dog("danny");
d.sayHello(); // dog danny
因為 Dog.__proto__
是一個 Function
,所以 Dog.__proto__
會等於 Function.prototype
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
Dog.prototype.sayHello = function () {
console.log("dog", this.name);
};
Object.prototype.sayHello = function () {
console.log("object", this.name);
};
var d = new Dog("danny");
console.log(Dog.__proto__); // Function
console.log(Dog.__proto__ === Function.prototype); // true
String.prototype
var a = "friday";
a.toString();
console.log(a.__proto__ === String.prototype); // true
console.log(a.toString === String.prototype.toString); // true
字串 a
本身並沒有 toString
這個 method 可以用
因為 a
是一個 String,所以 a.__proto__
會等於 String.prototype
toString
這個 function 是放在 prototype
上面的
當我 call a.toString
時,其實是在 call String.prototype.toString
在 String.prototype
加上自訂的 function
我只要在 String.prototype
加上 first
這樣的一個 function,
任何一個字串就都可以用 first
這個 function 來取得字串的第 0 個字元
這裡的 this
指向的就是「呼叫 first
的字串」
// 取得字串的第 0 個字元
String.prototype.first = function () {
return this[0];
};
var a = "friday";
console.log(a.first()); // f
所以,new 到底做了什麼事?
先補充一個預備知識:
test.call()
是另一種呼叫 function 的方式,call()
小括號裡面可以傳參數進去
如果我在第一個參數傳 123 進去,印出來的 this
就會是 123
意思就是
當我用 call()
來呼叫 function 時,在 call()
裡面傳入的第一個參數,就會是 function 裡面的 this
function test() {
console.log(this);
}
test.call("123"); // [String: '123']
自己實作出 new 的機制
newDog()
要做的事情跟「new
這個關鍵字」一樣,這樣 b.sayHello()
才有辦法跑
第一步:Dog.call(obj, name)
這行 Dog.call(obj, name)
,就會去執行 Dog
這個 constructor 函式,因為 Dog
裡面的 this
會是 obj
,所以就是 obj.name = name
,所以在 obj
這個物件裡面就會有 name: 'ben'
這個資料了
obj
這個物件就是「在執行完 constructor 之後,可以把我在 newDog(name)
傳入的 name
放在裡面」
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
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("danny");
var b = newDog("ben"); // newDog() 要做的事情跟「new 這個關鍵字」一樣
// b.sayHello();
function newDog(name) {
var obj = {};
Dog.call(obj, name); // 執行 constructor,在 Dog 裡面的 this 會是 obj
console.log(obj); // { name: 'ben' }
}
第二步:建立 obj.__proto__ = Dog.prototype
的連結
把 obj.__proto__
給指定到 Dog.prototype
,建立連結後,obj
也可以使用 Dog.prototype
上面的 function 了
第三步:把 obj
回傳回去
因為 var b = newDog("ben")
,newDog("ben")
最後會 return obj
-> 所以,obj
就會等同於 b
,也就等同於是 Dog
的一個 instance 了
obj.__proto__ = Dog.prototype
這行就會等於是 b.__proto__ = Dog.prototype
所以,b
就可以去使用 Dog.prototype
上面的 sayHello
了,b.sayHello()
就可以跑了!(會印出我傳入的 ben
)
// ES5 的物件導向
// Dog 函式,等同於是 ES6 的 constructor
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("danny"); // Dog 裡面的 this 會是 d
var b = newDog("ben"); // newDog() 要變成是 Dog 的一個 instance,要做的事情跟「new 這個關鍵字」一樣
b.sayHello(); // ben
function newDog(name) {
var obj = {}; // 產生一個新的 object
Dog.call(obj, name); // 執行 constructor(也就是 Dog 函式),把 obj 當作 this 丟進 constructor 裡面
// console.log(obj); // { name: 'ben' }
obj.__proto__ = Dog.prototype; // 建立連結,讓 obj 也可以使用 Dog.prototype 上面的 function
return obj; // 讓 obj 等同於 b
}
new 幫我做的事情
看完上面的範例後,可以知道 new 這個關鍵字幫我做的就是下面這幾件事情:
- 產生一個新的 object
var obj = {};
- 幫我去呼叫 constructor 函式(在這裡的 constructor 就是
Dog
函式)Dog.call(obj, name)
- 把新產生的 object 當作
this
丟進 constructor 裡面function Dog(name) { this.name = name; }
- 幫我設定好
__proto__
,讓obj.__proto__
去連到Dog.prototype
,這樣b
才可以使用sayHello
這個 functionobj.__proto__ = Dog.prototype;
- 把 object 回傳回去,讓 object 等於
b
(也就是Dog
的一個 instance)return obj;
物件導向的繼承:Inheritance
物件導向有一個很有名的概念,叫做「繼承」
繼承的使用時機
需要用到一些共同的屬性時,就可以用繼承的方式(就不用所有東西都自己重新做)
範例:
有一種特殊品種的狗叫做 BlackDog
第 13 行 class BlackDog extends Dog
就是「讓 BlackDog
去繼承 Dog
」
BlackDog
繼承 Dog
之後,就可以使用 Dog
的每一個 method (function) 了!
就像是 BlackDog
把 Dog
的東西都拿過來了,所以 BlackDog
可以使用 constructor
, sayHello
, 以及自己的 test
當執行到第 19 行 const d = new BlackDog("danny")
- 因為在
BlackDog
裡面沒有寫 constructor,所以就會往上層的 parent 找(因為BlackDog
繼承了Dog
,所以BlackDog
的 parent 就是Dog
),在Dog
找到了 constructor 後,就執行 constructor - 執行完 constructor 之後,
BlackDog
就擁有this.name
了
當執行到第 21 行 d.test()
時,就會印出 test
裡面的 this.name
也可以用 d.sayHello()
來印出 this.name
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
d.test(); // test! danny
d.sayHello(); // danny
現在,我要做的功能是:在 BlackDog
被建立時,就呼叫 sayHello
錯誤寫法
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
// 錯誤寫法
constructor() {
this.sayHello(); // 在 BlackDog 被建立時,就呼叫 sayHello
}
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");
執行之後會噴出錯誤「ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor」
意思就是:
在存取 this
之前,一定要先 call super
而 super
就是「上一層的 constructor」,也就是「Dog
的 constructor」
為什麼一定要先 call super
呢?
原因為:
如果沒有先 call super
的話,
在執行 BlackDog
裡面的 this.sayHello()
時,在 sayHello
裡面會用到 this.name
,但是這時還沒有執行到 Dog
的 constructor,所以 this.name
還沒被初始化,就會造成 bug
因此,就強制一定要先 call super
可是,只有 call super
是沒用的
因為這時在 Dog
的 constructor
接收的 name
會是 undefined
那既然我已經在 BlackDog
複寫一個 constructor 了,那在 BlackDog
的 constructor 就要負責接收 name
,並且用 super(name)
把 name
也傳到 parent 的 constructor 去,讓 Dog
的 constructor 可以成功的初始化
這樣當我在 BlackDog
裡面 call this.sayHello
時,才會印出正確的 this.name
正確寫法:一定要先 call super
,才能存取到 this
// ES6 的物件導向
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
// BlackDog 繼承了 Dog
class BlackDog extends Dog {
// 正確寫法
constructor(name) {
super(name); // 就是 Dog 的 constructor,把 name 傳到 Dog 的 constructor 去
this.sayHello(); // 在 BlackDog 被建立時,就呼叫 sayHello(會印出 this.name)
}
test() {
console.log("test!", this.name);
}
}
const d = new BlackDog("danny");