一、前言
介紹構造函數,原型,原型鏈。比如說經常會被問道: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交流學習社區)