一、前言
介紹建構函式,原型,原型鏈。比如說經常會被問道:symbol是不是建構函式;constructor屬性是否只讀;prototype、[[Prototype]]和__proto__的區別;什麼是原型鏈?等等問題
二、建構函式
1、什麼建構函式
建構函式就是通過new關鍵詞生成例項的函式。
js的建構函式和其他語言不一樣,一般規範都是首字母大寫。
首先我們來看一下這個栗子:
// saucxs
function Parent(age) {
this.age = age;
}
var p = new Parent(30);
console.log(p); //見下圖
console.log(p.constructor); // ƒ Parent(age){this.age = age;}
p.constructor === Parent; // true
p.constructor === Object; // false
這就是一個典型的建構函式,建構函式本身也是個函式,與普通區別不大,主要區別就是:建構函式使用new生成例項,直接呼叫就是普通函式。
2、constructor屬性
返回建立例項物件的Object建構函式的引用。此屬性的值對函式本身的引用,而不是一個包含函式名稱的字串。
所有物件都會從它的原型上繼承一個constructor屬性:
var o = {};
o.constructor === Object; // true
var o = new Object;
o.constructor === Object; // true
var a = [];
a.constructor === Array; // true
var a = new Array;
a.constructor === Array // true
var n = new Number(3);
n.constructor === Number; // true
那麼普通函式建立的例項有沒有constructor屬性呢?
// saucxs
// 普通函式
function parent2(age) {
this.age = age;
}
var p2 = parent2(50);
console.log(p2);
// undefined
// 普通函式
function parent3(age) {
return {
age: age
}
}
var p3 = parent3(50);
console.log(p3.constructor); //ƒ Object() { [native code] }
p3.constructor === parent3; // false
p3.constructor === Object; // true
上面程式碼說明:
(1)普通函式在內部有return操作的就有constructor屬性,沒有return的沒有constructor屬性;
(2)有constructor屬性的普通函式的constructor屬性值不是普通函式本身,是Object。
3、symbol是建構函式嗎?
MDN 是這樣介紹 `Symbol` 的
The `Symbol()` function returns a value of type **symbol**, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "`new Symbol()`".
Symbol是基本資料型別,作為建構函式它不完整,因為不支援語法new Symbol(),如果要生成例項直接使用Symbol()就可以的。
// saucxs
new Symbol(123); // Symbol is not a constructor
Symbol(123); // Symbol(123)
雖然Symbol是基本資料型別,但是Symbol(1234)例項可以獲取constructor屬性值。
// saucxs
var sym = Symbol(123);
console.log( sym ); // Symbol(123)
console.log( sym.constructor ); // ƒ Symbol() { [native code] }
sym.constructor === Symbol; //true
sym.constructor === Object; //false
這裡的constructor屬性來自哪裡?其實是Symbol原型上的,預設為Symbol函式。
4、constructor的值是隻讀的嗎?
回答:如果是引用型別的constructor屬性值是可以修改的,如果是基本型別的就是隻讀的。
引用型別的情況,修改這個很好理解,比如原型鏈繼承的方案中,就是對constructor重新賦值的修正。
// saucxs
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// 設定 Bar 的 prototype 屬性為 Foo 的例項物件
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
Bar.prototype.constructor === Object;
//true
// 修正 Bar.prototype.constructor 為 Bar 本身
Bar.prototype.constructor = Bar;
var test = new Bar() // 建立 Bar 的一個新例項
console.log(test);
對於基本型別來說是隻讀的,比如:1, "saucxs", true, Symbol, null, undefined。null和undefined也是沒有constructor屬性的。
// saucxs
function Type() { };
var types = [1, "muyiy", true, Symbol(123)];
for(var i = 0; i < types.length; i++) {
types[i].constructor = Type;
types[i] = [ types[i].constructor, types[i] instanceof Type, types[i].toString() ];
};
console.log( types.join("\n") );
// function Number() { [native code] },false,1
// function String() { [native code] },false,muyiy
// function Boolean() { [native code] },false,true
// function Symbol() { [native code] },false,Symbol(123)
為什麼會這樣?因為建立他們的是隻讀的原生建構函式(native constructors),這個栗子說明依賴一個物件的constructor屬性並不安全。
三、原型
3.1 prototype屬性
每一個物件都擁有一個原型物件,物件以其原型為模板,從原型整合方法和屬性,這些屬相和方法都在物件的構造器函式的prototype屬性上,而不是物件例項本身上。
上圖發現:
1、Parent物件有一個原型物件Parent.prototype,原型物件上有兩個屬性,分別為:constructor和__proto__,其中__proto__已被棄用。
2、建構函式Parent有一個指向原型的指標constructor;原型Parent.prototype有一個指向建構函式的指標Parent.prototype.constrcutor,其實就是一個迴圈引用。
3.2 __proto__屬性
上圖中可以看到Parent原型(Parent.prototype)上有一個__proto__屬性,這是一個訪問器屬性(即getter函式和setter函式)。作用:通過__proto__可以訪問到物件的內部[[Prototype]](一個物件或者null)
`__proto__` 發音 dunder proto,最先被 Firefox使用,後來在 ES6 被列為 Javascript 的標準內建屬性。
`[[Prototype]]` 是物件的一個內部屬性,外部程式碼無法直接訪問。
// saucxs
function Parent(){};
var p = new Parent();
console.log(p);
console.log(Parent.prototype);
1、p.__proto__獲取的是物件的原型,__proto__是每一個例項上都有的屬性;
2、prototype是建構函式的屬性;
3、p.__proto__和Parent.prototype指向同一個物件。
// saucxs
function Parent() {}
var p = new Parent();
p.__proto__ === Parent.prototype
// true
所以建構函式Parent,Parent.prototype和p之間的關係,如下圖所示:
注意1:`__proto__` 屬性在 `ES6` 時才被標準化
以確保 Web 瀏覽器的相容性,但是不推薦使用,除了標準化的原因之外還有效能問題。為了更好的支援,推薦使用 `Object.getPrototypeOf()`。
如果要讀取或修改物件的 `[[Prototype]]` 屬性,建議使用如下方案,但是此時設定物件的 `[[Prototype]]` 依舊是一個緩慢的操作,如果效能是一個問題,就要避免這種操作。
如果要建立一個新物件,同時繼承另一個物件的 `[[Prototype]]` ,推薦使用 `Object.create()`。
// saucxs
function Parent() {
age: 50
};
var p = new Parent();
var child = Object.create(p);
這裡 `child` 是一個新的空物件,有一個指向物件 p 的指標 `__proto__`。
四、原型鏈
每一個物件擁有一個原型物件,通過__proto__指標指向上一個原型,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層層的,最終指向null。這種關係成為原型鏈(prototype chain),作用:通過原型鏈一個物件會擁有定義在其他物件中的屬性和方法。
// saucxs
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true
p.constructor指向Parent,那麼是不是意味著p例項化存在constructor屬性呢?並不存在,列印一下p:
有圖可以知道,例項化物件p本身沒有constructor屬性,是通過原型鏈向上查詢__proto__,最終找到constructor屬性,該屬性指向Parent
// saucxs
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p; // Parent {age: 50}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true
下圖展示原型鏈執行機制。
五、總結
1、Symbol是基本資料型別,作為建構函式並不完整,因為不支援語法new Symbol(),但是原型上擁有constructor屬性,即Symbol.prototype.constructor。
2、引用型別constructor屬性值是可以修改的,但是對於基本型別的是隻讀的,當然null和undefined沒有constructor屬性。
3、__proto__是每個例項上都有的屬性,prototype是建構函式的屬性,這兩個不一樣,但是p.__proto__和Parent.prototype是指向同一個物件。
4、__proto__屬性在ES6時被標準化,但是因為效能問題並不推薦使用,推薦使用Object.getPropertyOf()。
5、每個物件擁有一個原型物件,通過__ptoto_指標指向上一個原型,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層已成的,最終指向null,這就是原型鏈。
六、參考
1、原型物件
2、Objcet.prototype.constructor
4、Symbol
5、原型
文章首發地址(sau交流學習社群)