引言
JS
系列暫定 27 篇,從基礎,到原型,到非同步,到設計模式,到架構模式等,
本篇是 JS
系列中最重要的一章,花費 3 分鐘即可理解,如果你已瞭解,快速瀏覽即可。
本篇文章主講建構函式、原型以及原型鏈,包括 Symbol
是不是建構函式、constructor
屬性是否只讀、prototype
、__proto__
、[[Prototype]]
、原型鏈。
一、基礎入門
1. 物件
在JS中,萬物皆物件,物件又分為普通物件和函式物件,其中 Object、Function 為 JS 自帶的函式物件。
let obj1 = {};
let obj2 = new Object();
let obj3 = new fun1()
function fun1(){};
let fun2 = function(){};
let fun3 = new Function('some','console.log(some)');
// JS自帶的函式物件
console.log(typeof Object); //function
console.log(typeof Function); //function
// 普通物件
console.log(typeof obj1); //object
console.log(typeof obj2); //object
console.log(typeof obj3); //object
// 函式物件
console.log(typeof fun1); //function
console.log(typeof fun2); //function
console.log(typeof fun3); //function
複製程式碼
凡是通過 new Function()
建立的物件都是函式物件,其他的都是普通物件,Function Object 是通過 New Function()
建立的。
2. 建構函式
function Foo(name, age) {
// this 指向 Foo
this.name = name
this.age = age
this.class = 'class'
// return this // 預設有這一行
}
// Foo 的例項
let f = new Foo('aa', 20)
複製程式碼
每個例項都有一個 constructor
(建構函式)屬性,該屬性指向物件本身。
f.constructor === Foo // true
複製程式碼
建構函式本身就是一個函式,與普通函式沒有任何區別,不過為了規範一般將其首字母大寫。建構函式和普通函式的區別在於,使用 new
生成例項的函式就是建構函式,直接呼叫的就是普通函式。
JS 本身不提供一個 class
實現。(在 ES2015/ES6 中引入了 class
關鍵字,但只是語法糖,JavaScript 仍然是基於原型的)。
3. 建構函式擴充套件
let a = {}
其實是let a = new Object()
的語法糖let a = []
其實是let a = new Array()
的語法糖function Foo(){ ... }
其實是var Foo = new Function(...)
- 可以使用
instanceof
判斷一個函式是否為一個變數的建構函式
4. Symbol 是建構函式嗎?
Symbol 是基本資料型別,它並不是建構函式,因為它不支援 new Symbol()
語法。我們直接使用Symbol()
即可。
let an = Symbol("An");
let an1 = new Symbol("An");
// Uncaught TypeError: Symbol is not a constructor
複製程式碼
但是,Symbol()
可以獲取到它的 constructor 屬性
Symbol("An").constructor;
// ƒ Symbol() { [native code] }
複製程式碼
這個 constructor
實際上是 Symbol 原型上的,即
Symbol.prototype.constructor;
// ƒ Symbol() { [native code] }
複製程式碼
對於 Symbol,你還需要了解以下知識點:
Symbol()
返回的 symbol 值是唯一的
Symbol("An") === Symbol("An");
// false
複製程式碼
可以通過 Symbol.for(key)
獲取全域性唯一的 symbol
Symbol.for('An') === Symbol.for("An"); // true
複製程式碼
它從執行時的 symbol 登錄檔中找到對應的 symbol,如果找到了,則返回它,否則,新建一個與該鍵關聯的 symbol,並放入全域性 symbol 登錄檔中。
Symbol.iterator :返回一個物件的迭代器
// 實現可迭代協議,使迭代器可迭代:Symbol.iterator
function createIterator(items) {
let i = 0
return {
next: function () {
let done = (i >= items.length)
let value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
},
[Symbol.iterator]: function () {
return this
}
}
}
const iterator = createIterator([1, 2, 3]);
[...iterator]; // [1, 2, 3]
複製程式碼
Symbol.toPrimitive:將物件轉換成基本資料型別
// Symbol.toPrimitive 來實現拆箱操作(ES6 之後)
let obj = {
valueOf: () => {console.log("valueOf"); return {}},
toString: () => {console.log("toString"); return {}}
}
obj[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(obj + "")
// toPrimitive
// hello
複製程式碼
Symbol.toStringTag:用於設定物件的預設描述字串值
// Symbol.toStringTag 代替 [[class]] 屬性(ES5開始)
let o = { [Symbol.toStringTag]: "MyObject" }
console.log(o + "");
// [object MyObject]
複製程式碼
5. constructor 的值是隻讀的嗎?
對於引用型別來說 constructor
屬性值是可以修改的,但是對於基本型別來說是隻讀的。
引用型別
function An() {
this.value = "An";
};
function Anran() {};
Anran.prototype.constructor = An;
// 原型鏈繼承中,對 constructor 重新賦值
let anran = new Anran();
// 建立 Anran 的一個新例項
console.log(anran);
複製程式碼
這說明,依賴一個引用物件的 constructor 屬性,並不是安全的。
基本型別
function An() {};
let an = 1;
an.constructor = An;
console.log(an.constructor);
// ƒ Number() { [native code] }
複製程式碼
這是因為:原生建構函式(native constructors
)是隻讀的。
JS 對於不可寫的屬性值的修改靜默失敗(silently failed),但只會在嚴格模式下才會提示錯誤。
'use strict';
function An() {};
let an = 1;
an.constructor = An;
console.log(an.constructor);
複製程式碼
注意:null
和 undefined
是沒有 constructor
屬性的。
二、原型
首先,貼上
圖片來自於http://www.mollypages.org/tutorials/js.mp,請根據下文仔細理解這張圖
在JS中,每個物件都有自己的原型。當我們訪問物件的屬性和方法時,JS 會先訪問物件本身的方法和屬性。如果物件本身不包含這些屬性和方法,則訪問物件對應的原型。
// 建構函式
function Foo(name) {
this.name = name
}
Foo.prototype.alertName = function() {
alert(this.name)
}
// 建立例項
let f = new Foo('some')
f.printName = function () {
console.log(this.name)
}
// 測試
f.printName()// 物件的方法
f.alertName()// 原型的方法
複製程式碼
1. prototype
所有函式都有一個 prototype
(顯式原型)屬性,屬性值也是一個普通的物件。物件以其原型為模板,從原型繼承方法和屬性,這些屬性和方法定義在物件的構造器函式的 prototype
屬性上,而非物件例項本身。
但有一個例外: Function.prototype.bind()
,它並沒有 prototype 屬性
let fun = Function.prototype.bind();
// ƒ () { [native code] }
複製程式碼
當我們建立一個函式時,例如
function Foo () {}
複製程式碼
prototype
屬性就被自動建立了
從上面這張圖可以發現,Foo
物件有一個原型物件 Foo.prototype
,其上有兩個屬性,分別是 constructor
和 __proto__
,其中 __proto__
已被棄用。
建構函式 Foo
有一個指向原型的指標,原型 Foo.prototype
有一個指向建構函式的指標 Foo.prototype.constructor
,這就是一個迴圈引用,即:
Foo.prototype.constructor === Foo; // true
複製程式碼
2. __proto__
每個例項物件(object )都有一個隱式原型屬性(稱之為 __proto__
)指向了建立該物件的建構函式的原型。也就時指向了函式的 prototype
屬性。
function Foo () {}
let foo = new Foo()
複製程式碼
當 new Foo()
時,__proto__
被自動建立。並且
foo.__proto__ === Foo.prototype; // true
複製程式碼
即:
__proto__
發音 dunder proto,最先被 Firefox使用,後來在 ES6 被列為 Javascript 的標準內建屬性。
3. [[Prototype]]
[[Prototype]]
是物件的一個內部屬性,外部程式碼無法直接訪問。
遵循 ECMAScript 標準,someObject.[[Prototype]] 符號用於指向 someObject 的原型
4. 注意
__proto__
屬性在 ES6
時才被標準化,以確保 Web 瀏覽器的相容性,但是不推薦使用,除了標準化的原因之外還有效能問題。為了更好的支援,推薦使用 Object.getPrototypeOf()
。
通過現代瀏覽器的操作屬性的便利性,可以改變一個物件的
[[Prototype]]
屬性, 這種行為在每一個JavaScript引擎和瀏覽器中都是一個非常慢且影響效能的操作,使用這種方式來改變和繼承屬性是對效能影響非常嚴重的,並且效能消耗的時間也不是簡單的花費在obj.__proto__ = ...
語句上, 它還會影響到所有繼承來自該[[Prototype]]
的物件,如果你關心效能,你就不應該在一個物件中修改它的 [[Prototype]]。相反, 建立一個新的且可以繼承[[Prototype]]
的物件,推薦使用Object.create()
。
如果要讀取或修改物件的 [[Prototype]]
屬性,建議使用如下方案,但是此時設定物件的 [[Prototype]]
依舊是一個緩慢的操作,如果效能是一個問題,就要避免這種操作。
// 獲取(兩者一致)
Object.getPrototypeOf()
Reflect.getPrototypeOf()
// 修改(兩者一致)
Object.setPrototypeOf()
Reflect.setPrototypeOf()
複製程式碼
如果要建立一個新物件,同時繼承另一個物件的 [[Prototype]]
,推薦使用 Object.create()
。
function An() {};
var an = new An();
var anran = Object.create(an);
複製程式碼
這裡 anran
是一個新的空物件,有一個指向物件 an
的指標 __proto__
。
5. new 的實現過程
-
新生成了一個物件
-
連結到原型
-
繫結 this
-
返回新物件
function new_object() {
// 建立一個空的物件
let obj = new Object()
// 獲得建構函式
let Con = [].shift.call(arguments)
// 連結到原型 (不推薦使用)
obj.__proto__ = Con.prototype
// 繫結 this,執行建構函式
let result = Con.apply(obj, arguments)
// 確保 new 出來的是個物件
return typeof result === 'object' ? result : obj
}
複製程式碼
優化 new 實現
// 優化後 new 實現
function create() {
// 1、獲得建構函式,同時刪除 arguments 中第一個引數
Con = [].shift.call(arguments);
// 2、建立一個空的物件並連結到原型,obj 可以訪問建構函式原型中的屬性
let obj = Object.create(Con.prototype);
// 3、繫結 this 實現繼承,obj 可以訪問到建構函式中的屬性
let ret = Con.apply(obj, arguments);
// 4、優先返回建構函式返回的物件
return ret instanceof Object ? ret : obj;
};
複製程式碼
6. 總結
- 所有的引用型別(陣列、物件、函式)都有物件特性,即可自由擴充套件屬性(null除外)。
- 所有的引用型別,都有一個
__proto__
屬性,屬性值是一個普通的物件,該原型物件也有一個自己的原型物件(__proto__
) ,層層向上直到一個物件的原型物件為null
。根據定義,null
沒有原型,並作為這個原型鏈 中的最後一個環節。 - 當試圖得到一個物件的某個屬性時,如果這個物件本身沒有這個屬性,那麼會去它的
__proto__
(即它的建構函式的prototype
)中尋找。
三、原型鏈
每個物件擁有一個原型物件,通過 __proto__
指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,最終指向 null
,這種關係被稱為原型鏈(prototype chain)。根據定義,null
沒有原型,並作為這個原型鏈中的最後一個環節。
原型鏈的基本思想是利用原型,讓一個引用型別繼承另一個引用型別的屬性及方法。
// 建構函式
function Foo(name) {
this.name = name
}
// 建立例項
let f = new Foo('some')
// 測試
f.toString()
// f.__proto__.__proto__中尋找
複製程式碼
f.__proto__=== Foo.prototype
,Foo.prototype
也是一個物件,也有自己的__proto__
指向 Object.prototype
, 找到toString()
方法。
也就是
Function.__proto__.__proto__ === Object.prototype
複製程式碼
下面是原型鏈繼承的例子
function Elem(id) {
this.elem = document.getElementById(id)
}
Elem.prototype.html = function(val) {
let elem = this.elem
if (val) {
elem.innerHTML = val
return this // 鏈式操作
} else {
return elem.innerHTML
}
}
Elem.prototype.on = function( type, fn) {
let elem = this.elem
elem.addEventListener(type, fn)
}
let div1 = new Elem('div1')
// console.log(div1.html())
div1.html('<p>hello</p>').on('click', function() {
alert('clicked')
})// 鏈式操作
複製程式碼
四、總結
Symbol
是基本資料型別,並不是建構函式,因為它不支援語法new Symbol()
,但其原型上擁有constructor
屬性,即Symbol.prototype.constructor
。- 引用型別
constructor
是可以修改的,但對於基本型別來說它是隻讀的,null
和undefined
沒有constructor
屬性。 __proto__
是每個例項物件都有的屬性,prototype
是其建構函式的屬性,在例項上並不存在,所以這兩個並不一樣,但foo.__proto__
和Foo.prototype
指向同一個物件。__proto__
屬性在ES6
時被標準化,但因為效能問題並不推薦使用,推薦使用Object.getPrototypeOf()
。- 每個物件擁有一個原型物件,通過
__proto__
指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層向上,最終指向null
,這就是原型鏈。 - 當試圖得到一個物件的某個屬性時,如果這個物件本身沒有這個屬性,那麼會去它的原型中尋找,以及該物件的原型的原型,一層一層向上查詢,直到找到一個名字匹配的屬性 / 方法或到達原型鏈的末尾(
null
)
五、參考
暫時就這些,後續我將持續更新
系列文章
- JS 系列一:var、let、const、解構、展開、new、this、class、函式
- JS 系列二:深入 constructor、prototype、proto、[[Prototype]] 及 原型鏈
- JS 系列三:繼承的 六 種實現方式
- JS 系列四:深入剖析 instanceof 運算子
想看更過系列文章,點選前往 github 部落格主頁
走在最後
1. 如有任何問題或更獨特的見解,歡迎評論或直接聯絡瓶子君(公眾號回覆 123 即可)!
2. 歡迎關注:前端瓶子君,每日更新!