摘要:一般我們對(duì)這種構(gòu)造函數(shù)命名都會(huì)采用,并把它稱呼為類,這不僅是為了跟的理念保持一致,也是因?yàn)榈膬?nèi)建類也是這種命名。由生成的對(duì)象,其是。這是標(biāo)準(zhǔn)的規(guī)定。本文的主題是原型系統(tǒng)的變遷,所以并沒有涉及和對(duì)原型鏈的影響。
概述
JavaScript 的原型系統(tǒng)是最初就有的語言設(shè)計(jì)。但隨著 ES 標(biāo)準(zhǔn)的進(jìn)化和新特性的添加。它也一直在不停進(jìn)化。這篇文章的目的就是梳理一下早期到 ES5 和現(xiàn)在 ES6,新特性的加入對(duì)原型系統(tǒng)的影響。
如果你對(duì)原型的理解還停留在 function + new 這個(gè)層面而不知道更深入的操作原型鏈的技巧,或者你想了解 ES6 class 的知識(shí),相信本文會(huì)有所幫助。
這篇文章是我學(xué)習(xí) You Don"t Know JS 的副產(chǎn)品,推薦任何想系統(tǒng)性地學(xué)習(xí) JavaScript 的人去閱讀此書。
JavaScript 原型簡(jiǎn)述很多人應(yīng)該都對(duì)原型(prototype)不陌生。簡(jiǎn)單地說,JavaScript 是基于原型的語言。當(dāng)我們調(diào)用一個(gè)對(duì)象的屬性時(shí),如果對(duì)象沒有該屬性,JavaScript 解釋器就會(huì)從對(duì)象的原型對(duì)象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型。這種屬性查找的方式被稱為原型鏈(prototype chain)。
對(duì)象的原型是沒有公開的屬性名去訪問的(下文再談 __proto__ 屬性)。以下為了方便稱呼,我把一個(gè)對(duì)象內(nèi)部對(duì)原型的引用稱為 [[Prototype]]。
JavaScript 沒有類的概念,原型鏈的設(shè)定就是少數(shù)能夠讓多個(gè)對(duì)象共享屬性和方法,甚至模擬繼承的方式。在 ES5 以前,如果我們想設(shè)置對(duì)象的 [[Prototype]],只能通過 new 關(guān)鍵字,比如:
function User() { this._name = "David" } User.prototype.getName = function() { return this._name } var user = new User() user.getName() // "David" user.hasOwnProperty("getName") // false
當(dāng) User 函數(shù)被 new 關(guān)鍵字調(diào)用時(shí),它就類似于一個(gè)構(gòu)造函數(shù),其生成的對(duì)象的 [[Prototype]] 會(huì)引用 User.prototype 。因?yàn)?User.prototype 也是一個(gè)對(duì)象,它的 [[Prototype]] 是 Object.prototype 。
一般我們對(duì)這種構(gòu)造函數(shù)命名都會(huì)采用 CamelCase ,并把它稱呼為“類”,這不僅是為了跟 OOP 的理念保持一致,也是因?yàn)?JavaScript 的內(nèi)建“類”也是這種命名。
由 SomeClass 生成的對(duì)象,其 [[Prototype]] 是 SomeClass.prototype。除了稍顯繁瑣,這套邏輯是可以自圓其說的,比如:
我們用 {..} 創(chuàng)建的對(duì)象的 [[Prototype]] 都是 Object.prototype,也是原型鏈的頂點(diǎn)。
數(shù)組的 [[Prototype]] 是 Array.prototype 。
字符串的 [[Prototype]] 是 String.prototype 。
Array.prototype 和 String.prototype 的 [[Prototype]] 是 Object.prototype 。
模擬繼承模擬繼承是自定義原型鏈的典型使用場(chǎng)景。但如果用 new 的方式則比較麻煩。一種常見的解法是:子類的 prototype 等于父類的實(shí)例。這就涉及到定義子類的時(shí)候調(diào)用父類的構(gòu)造函數(shù)。為了避免父類的構(gòu)造函數(shù)在類定義過程中的潛在影響,我們一般會(huì)建造一個(gè)臨時(shí)類去做代替父類 new 的過程。
function Parent() {} function Child() {} function createSubProto(proto) { // fn 在這里就是臨時(shí)類 var fn = function() {} fn.prototype = proto return new fn() } Child.prototype = createSubProto(Parent.prototype) Child.prototype.constructor = Child var child = new Child() child instanceof Child // true child instanceof Parent // trueES5: 自由地操控原型鏈
既然原型鏈本質(zhì)上只是建立對(duì)象之間的關(guān)聯(lián),那我們可不可以直接操作對(duì)象的 [[Prototype]] 呢?
在 ES5(準(zhǔn)確的說是 5.1)之前,我們沒有辦法直接獲取對(duì)象的原型,只能通過 [[Prototype]] 的 constructor。
var user = new User() user.constructor.prototype // User user.hasOwnProperty("constructor") // false
類可以通過 prototype 屬性獲取生成的對(duì)象的 [[Prototype]]。[[Prototype]] 里的 constructor 屬性又會(huì)反過來引用函數(shù)本身。因?yàn)?user 的原型是 User.prototype ,它自然也能夠通過 constructor 獲取到 User 函數(shù),進(jìn)而獲取到自己的 [[Prototype]]。比較繞是吧?
ES5.1 之后加了幾個(gè)新的 API 幫助我們操作對(duì)象的 [[Prototype]],自此以后 JavaScript 才真的有自由操控原型的能力。它們是:
Object.prototype.isPrototypeOf
Object.create
Object.getPrototypeOf
Object.setPrototypeOf
注:以上方法并不完全是 ES5.1 的,isPrototypeOf 是 ES3 就有的,setPrototypeOf 是 ES6 才有的。但它們的規(guī)范都在 ES6 中修改了一部分。
下面的例子里,Object.create 創(chuàng)建 child 對(duì)象,并把 [[Prototype]] 設(shè)置為 parent 對(duì)象。Object.getPrototypeOf 可以直接獲取對(duì)象的 [[Prototype]]。isPrototypeOf 能夠判斷一個(gè)對(duì)象是否在另一個(gè)對(duì)象的原型鏈上。
var parent = { _name: "David", getName: function() { return this._name }, } var child = Object.create(parent) Object.getPrototypeOf(child) // parent parent.isPrototypeOf(child) // true Object.prototype.isPrototypeOf(child) // true child instanceof Object // true
既然有 Object.getPrototypeOf,自然也有 Object.setPrototypeOf 。這個(gè)函數(shù)可以修改任何對(duì)象的 [[Prototype]] ,包括內(nèi)建類型。
var anotherParent = { name: "Alex" } Object.setPrototypeOf(child, anotherParent) Object.getPrototypeOf(child) // anotherParent // 修改數(shù)組的 [[Prototype]] var a = [] Object.setPrototypeOf(a, anotherParent) a instanceof Array // false Object.getPrototypeOf(a) // anotherParent
靈活使用以上的幾個(gè)方法,我們可以非常輕松地創(chuàng)建原型鏈,或者在已知原型鏈中插入自定義的對(duì)象,玩法只取決于想象力。我們以此修改一下上面的模擬繼承的例子:
function Parent() {} function Child() {} Child.prototype = Object.create(Parent.prototype) Child.prototype.constructor = Child
因?yàn)?Object.create(..) 傳入的參數(shù)會(huì)作為 [[Prototype]] ,所以這里有一個(gè)有意思的小技巧。我們可以用 Object.create(null) 創(chuàng)建一個(gè)沒有任何屬性的對(duì)象。這個(gè)技巧適合做 proxy 對(duì)象,有點(diǎn)類似 Ruby 中的 BasicObject。
尷尬的私生子 __proto__說到操作 [[Prototype]] 就不得不提 __proto__ 。這個(gè)屬性是一個(gè) getter/setter ,可以用來獲取和設(shè)置任意對(duì)象的 [[Prototype]] 。
child.__proto__ // equal to Object.getPrototypeOf(child) child.__proto__ = parent // equal to Object.setPrototypeOf(child, parent)
它本來不是 ES 的標(biāo)準(zhǔn),無奈眾多瀏覽器早早地都實(shí)現(xiàn)了這個(gè)屬性,而且應(yīng)用得還挺廣泛的。到了 ES6 為了向下兼容性只好接納它成為標(biāo)準(zhǔn)的一部分。這是典型的現(xiàn)實(shí)倒逼標(biāo)準(zhǔn)的例子。
看看 MDN 的描述都充滿了怨念。
The use of proto is controversial, and has been discouraged. It was never originally included in the EcmaScript language spec, but modern browsers decided to implement it anyway. Only recently, the proto property has been standardized in the ECMAScript 6 language specification for web browsers to ensure compatibility, so will be supported into the future. It is deprecated in favor of Object.getPrototypeOf/Reflect.getPrototypeOf and Object.setPrototypeOf/Reflect.setPrototypeOf (though still, setting the [[Prototype]] of an object is a slow operation that should be avoided if performance is a concern).
__proto__ 是不被推薦的用法。大部分情況下我們?nèi)匀粦?yīng)該用 Object.getPrototypeOf 和 Object.setPrototypeOf 。什么是少數(shù)情況,待會(huì)再講。
ES6: class 語法糖不得不說開發(fā)者世界受 OO 的影響非常之深,雖然 ES5 給了我們足夠靈活的 API ,但是:
很多人還是傾向于用 class 來組織代碼。
很多類庫、框架創(chuàng)造了自己的 API 來實(shí)現(xiàn) class 的功能。
產(chǎn)生這一現(xiàn)象的原因有很多,但事實(shí)如此。而且如果用別人的輪子,有些事是我們無法選擇的。也許是看到了這一現(xiàn)象,ES6 時(shí)代終于有了 class 語法,有望統(tǒng)一各個(gè)類庫和框架不一致的類實(shí)現(xiàn)方式。來看一個(gè)例子:
class User { constructor(firstName, lastName) { this.firstName = firstName this.lastName = lastName } fullName() { return `${this.firstName} ${this.lastName}` } } let user = new User("David", "Chen") user.fullName() // David Chen
以上的類定義語法非常直觀,它跟以下的 ES5 語法是一個(gè)意思:
function User(firstName, lastName) { this.firstName = firstName this.lastName = lastName } User.prototype.fullName = function() { return "" + this.firstName + this.lastName }
ES6 并沒有改變 JavaScript 基于原型的本質(zhì),只是在此之上提供了一些語法糖。class 就是其中之一。其他的還有 extends,super 和 static 。它們大多數(shù)都可以轉(zhuǎn)換成等價(jià)的 ES5 語法。
我們來看看另一個(gè)繼承的例子:
class Child extends Parent { constructor(firstName, lastName, age) { super(firstName, lastName) this.age = age } }
其基本等價(jià)于:
function Child(firstName, lastName, age) { Parent.call(this, firstName, lastName) this.age = age } Child.prototype = Object.create(Parent.prototype) Child.constructor = Child
無疑上面的例子更加直觀,代碼組織更加清晰。這也是加入新語法的目的。不過雖然新語法的本質(zhì)還是基于原型的,但新加入的概念或多或少會(huì)引起一些連帶的影響。
extends 繼承內(nèi)建類的能力因?yàn)檎Z言內(nèi)部設(shè)計(jì)原因,我們沒有辦法自定義一個(gè)類來繼承 JavaScript 的內(nèi)建類的。繼承類往往會(huì)有各種問題。ES6 的 extends 的最大的賣點(diǎn),就是不僅可以繼承自定義類,還可以繼承 JavaScript 的內(nèi)建類,比如這樣:
class MyArray extends Array { }
這種方式可以讓開發(fā)者繼承內(nèi)建類的功能創(chuàng)造出符合自己想要的類。所有 Array 已有的屬性和方法都會(huì)對(duì)繼承類生效。這確實(shí)是個(gè)不錯(cuò)的誘惑,也是繼承最大的吸引力。
但現(xiàn)實(shí)總是悲催的。extends 內(nèi)建類會(huì)引發(fā)一些奇怪的問題,很多屬性和方法沒辦法在繼承類中正常工作。舉個(gè)例子:
var a = new Array(1, 2, 3) a.length // 3 var b = new MyArray(1, 2, 3) b.length // 0
如果說語法糖可以用 Babel.js 這種 transpiler 去編譯成 ES5 解決 ,擴(kuò)充的 API 可以用 polyfill 解決,但是這種內(nèi)建類的繼承機(jī)制顯然是需要瀏覽器支持的。而目前唯一支持這個(gè)特性的瀏覽器是………… Microsoft Edge 。
好在這并不是什么致命的問題。大多數(shù)此類需求都可以用封裝類去解決,無非是多寫一點(diǎn) wrapper API 而已。而且個(gè)人認(rèn)為封裝和組合反而是比繼承更靈活的解決方案。
super 帶來的新概念(坑?) super 在 constructor 和普通方法里的不同在 constructor 里面,super 的用法是 super(..)。它相當(dāng)于一個(gè)函數(shù),調(diào)用它等于調(diào)用父類的 constructor 。但在普通方法里面,super 的用法是 super.prop 或者 super.method()。它相當(dāng)于一個(gè)指向?qū)ο蟮?[[Prototype]] 的屬性。這是 ES6 標(biāo)準(zhǔn)的規(guī)定。
class Parent { constructor(firstName, lastName) { this.firstName = firstName this.lastName = lastName } fullName() { return `${this.firstName} ${this.lastName}` } } class Child extends Parent { constructor(firstName, lastName, age) { super(firstName, lastName) this.age = age } fullName() { return `${super.fullName()} (${this.age})` } }
注意:Babel.js 對(duì)方法里調(diào)用 super(..) 也能編譯出正確的結(jié)果,但這應(yīng)該是 Babel.js 的 bug ,我們不該以此得出 super(..) 也可以在非 constructor 里用的結(jié)論。
super 在子類的 constructor 里必須先于 this 調(diào)用如果寫子類的 constructor 需要操作 this ,那么 super 必須先調(diào)用!這是 ES6 的規(guī)則。所以寫子類的 constructor 時(shí)盡量把 super 寫在第一行。
class Child extends Parent { constructor() { this.xxx() // invalid super() } }super 是編譯時(shí)確定,不是運(yùn)行時(shí)確定
什么意思呢?先看代碼:
class Child extends Parent { fullName() { super.fullName() } }
以上代碼中 fullName 方法的 ES5 等價(jià)代碼是:
fullName() { Parent.prototype.fullName.call(this) }
而不是
fullName() { Object.getPrototypeOf(this).fullName.call(this) }
這就是 super 編譯時(shí)確定的特性。不過為什么要這樣設(shè)計(jì)?個(gè)人理解是,函數(shù)的 this 只有在運(yùn)行時(shí)才能確定。因此在運(yùn)行時(shí)根據(jù) this 的原型鏈去獲得上層方法并不太符合 class 的常規(guī)思維,在某些情況下更容易產(chǎn)生錯(cuò)誤。比如 child.fullName.call(anotherObj) 。
super 對(duì) static 的影響,和類的原型鏈static 相當(dāng)于類方法。因?yàn)榫幾g時(shí)確定的特性,以下代碼中:
class Child extends Parent { static findAll() { return super.findAll() } }
findAll 的 ES5 等價(jià)代碼是:
findAll() { return Parent.findAll() }
static 貌似和原型鏈沒關(guān)系,但這不妨礙我們討論一個(gè)問題:類的原型鏈?zhǔn)窃鯓拥模课覜]查到相關(guān)的資料,不過我們可以測(cè)試一下:
Object.getPrototypeOf(Child) === Parent // true Object.getPrototypeOf(Parent) === Object // false Object.getPrototypeOf(Parent) === Object.prototype // false proto = Object.getPrototypeOf(Parent) typeof proto // function proto.toString() // function () {} proto === Object.getPrototypeOf(Object) // true proto === Object.getPrototypeOf(String) // true new proto() //TypeError: function () {} is not a constructor
可見自定義類的話,子類的 [[Prototype]] 是父類,而所有頂層類的 [[Prototype]] 都是同一個(gè)函數(shù)對(duì)象,不管是內(nèi)建類如 Object 還是自定義類如 Parent 。但這個(gè)函數(shù)是不能用 new 關(guān)鍵字初始化的。雖然這種設(shè)計(jì)沒有 Ruby 的對(duì)象模型那么巧妙,不過也是能夠自圓其說的。
直接定義 object 并設(shè)定 [[Prototype]]除了通過 class 和 extends 的語法設(shè)定 [[Prototype]] 之外,現(xiàn)在定義對(duì)象也可以直接設(shè)定 [[Prototype]] 了。這就要用到 __proto__ 屬性了。“定義對(duì)象并設(shè)置 [[Prototype]]” 是唯一建議用 __proto__ 的地方。另外,另外注意 super 只有在 method() {} 這種語法下才能用。
let parent = { method1() { .. }, method2() { .. }, } let child = { __proto__: parent, // valid method1() { return super.method1() }, // invalid method2: function() { return super.method2() }, }總結(jié)
JavaScript 的原型是很有意思的設(shè)計(jì),從某種程度上說它是更加純粹的面向?qū)ο笤O(shè)計(jì)(而不是面向類的設(shè)計(jì))。ES5 和 ES6 加入的 API 能更有效地操控原型鏈。語言層面支持的 class 也能讓忠于類設(shè)計(jì)的開發(fā)者用更加統(tǒng)一的方式去設(shè)計(jì)類。雖然目前 class 僅僅提供了一些基本功能。但隨著標(biāo)準(zhǔn)的進(jìn)步,相信它還會(huì)擴(kuò)充出更多的功能。
本文的主題是原型系統(tǒng)的變遷,所以并沒有涉及 getter/setter 和 defineProperty 對(duì)原型鏈的影響。想系統(tǒng)地學(xué)習(xí)原型,你可以去看 You Don"t Know JS: this & Object Prototypes 。
參考資料You Don"t Know JS: this & Object Prototypes
You Don"t Know JS: ES6 & Beyond
Classes in ECMAScript 6 (final semantics)
MDN: Object.prototype.__proto__
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/92343.html
摘要:因?yàn)椴僮鞣麆?chuàng)建的對(duì)象都繼承自構(gòu)造函數(shù)的屬性。繼承的實(shí)現(xiàn)中常用的繼承方式是組合繼承,也就是通過構(gòu)造函數(shù)和原型鏈繼承同時(shí)來模擬繼承的實(shí)現(xiàn)。 原文發(fā)布在我的博客 我們都知道 JavaScript 是一門基于原型的語言。當(dāng)我們調(diào)用一個(gè)對(duì)象本身沒有的屬性時(shí),JavaScript 就會(huì)從對(duì)象的原型對(duì)象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型,一直找原型鏈的末端也就是 Object....
摘要:插件開發(fā)前端掘金作者原文地址譯者插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡(jiǎn)單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實(shí)現(xiàn)文件分片斷點(diǎn)續(xù)傳。 Vue.js 插件開發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡(jiǎn)單的方式。插....
摘要:前端每周清單第期與模式變遷與優(yōu)化界面生成作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實(shí)踐深度閱讀開源項(xiàng)目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000013279448); 前端每周清單第 51 期: React Context A...
摘要:正文在年,框架的選擇并不少。特別的,通過思考這些框架分別如何處理狀態(tài)變化是很有用的。本文探索以下的數(shù)據(jù)綁定,的臟檢查的虛擬以及它與不可變數(shù)據(jù)結(jié)構(gòu)之間的聯(lián)系。當(dāng)狀態(tài)產(chǎn)生變化時(shí),只有真正需要更新的部分才會(huì)發(fā)生改變。 譯者言 近幾年可謂是 JavaScript 的大爆炸紀(jì)元,各種框架類庫層出不窮,它們給前端帶來一個(gè)又一個(gè)的新思想。從以前我們用的 jQuery 直接操作 DOM,到 Backb...
閱讀 640·2021-08-17 10:15
閱讀 1724·2021-07-30 14:57
閱讀 1978·2019-08-30 15:55
閱讀 2820·2019-08-30 15:55
閱讀 2708·2019-08-30 15:44
閱讀 670·2019-08-30 14:13
閱讀 2386·2019-08-30 13:55
閱讀 2592·2019-08-26 13:56