摘要:一般我們對這種構造函數命名都會采用,并把它稱呼為類,這不僅是為了跟的理念保持一致,也是因為的內建類也是這種命名。由生成的對象,其是。這是標準的規定。本文的主題是原型系統的變遷,所以并沒有涉及和對原型鏈的影響。
概述
JavaScript 的原型系統是最初就有的語言設計。但隨著 ES 標準的進化和新特性的添加。它也一直在不停進化。這篇文章的目的就是梳理一下早期到 ES5 和現在 ES6,新特性的加入對原型系統的影響。
如果你對原型的理解還停留在 function + new 這個層面而不知道更深入的操作原型鏈的技巧,或者你想了解 ES6 class 的知識,相信本文會有所幫助。
這篇文章是我學習 You Don"t Know JS 的副產品,推薦任何想系統性地學習 JavaScript 的人去閱讀此書。
JavaScript 原型簡述很多人應該都對原型(prototype)不陌生。簡單地說,JavaScript 是基于原型的語言。當我們調用一個對象的屬性時,如果對象沒有該屬性,JavaScript 解釋器就會從對象的原型對象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型。這種屬性查找的方式被稱為原型鏈(prototype chain)。
對象的原型是沒有公開的屬性名去訪問的(下文再談 __proto__ 屬性)。以下為了方便稱呼,我把一個對象內部對原型的引用稱為 [[Prototype]]。
JavaScript 沒有類的概念,原型鏈的設定就是少數能夠讓多個對象共享屬性和方法,甚至模擬繼承的方式。在 ES5 以前,如果我們想設置對象的 [[Prototype]],只能通過 new 關鍵字,比如:
function User() { this._name = "David" } User.prototype.getName = function() { return this._name } var user = new User() user.getName() // "David" user.hasOwnProperty("getName") // false
當 User 函數被 new 關鍵字調用時,它就類似于一個構造函數,其生成的對象的 [[Prototype]] 會引用 User.prototype 。因為 User.prototype 也是一個對象,它的 [[Prototype]] 是 Object.prototype 。
一般我們對這種構造函數命名都會采用 CamelCase ,并把它稱呼為“類”,這不僅是為了跟 OOP 的理念保持一致,也是因為 JavaScript 的內建“類”也是這種命名。
由 SomeClass 生成的對象,其 [[Prototype]] 是 SomeClass.prototype。除了稍顯繁瑣,這套邏輯是可以自圓其說的,比如:
我們用 {..} 創建的對象的 [[Prototype]] 都是 Object.prototype,也是原型鏈的頂點。
數組的 [[Prototype]] 是 Array.prototype 。
字符串的 [[Prototype]] 是 String.prototype 。
Array.prototype 和 String.prototype 的 [[Prototype]] 是 Object.prototype 。
模擬繼承模擬繼承是自定義原型鏈的典型使用場景。但如果用 new 的方式則比較麻煩。一種常見的解法是:子類的 prototype 等于父類的實例。這就涉及到定義子類的時候調用父類的構造函數。為了避免父類的構造函數在類定義過程中的潛在影響,我們一般會建造一個臨時類去做代替父類 new 的過程。
function Parent() {} function Child() {} function createSubProto(proto) { // fn 在這里就是臨時類 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: 自由地操控原型鏈
既然原型鏈本質上只是建立對象之間的關聯,那我們可不可以直接操作對象的 [[Prototype]] 呢?
在 ES5(準確的說是 5.1)之前,我們沒有辦法直接獲取對象的原型,只能通過 [[Prototype]] 的 constructor。
var user = new User() user.constructor.prototype // User user.hasOwnProperty("constructor") // false
類可以通過 prototype 屬性獲取生成的對象的 [[Prototype]]。[[Prototype]] 里的 constructor 屬性又會反過來引用函數本身。因為 user 的原型是 User.prototype ,它自然也能夠通過 constructor 獲取到 User 函數,進而獲取到自己的 [[Prototype]]。比較繞是吧?
ES5.1 之后加了幾個新的 API 幫助我們操作對象的 [[Prototype]],自此以后 JavaScript 才真的有自由操控原型的能力。它們是:
Object.prototype.isPrototypeOf
Object.create
Object.getPrototypeOf
Object.setPrototypeOf
注:以上方法并不完全是 ES5.1 的,isPrototypeOf 是 ES3 就有的,setPrototypeOf 是 ES6 才有的。但它們的規范都在 ES6 中修改了一部分。
下面的例子里,Object.create 創建 child 對象,并把 [[Prototype]] 設置為 parent 對象。Object.getPrototypeOf 可以直接獲取對象的 [[Prototype]]。isPrototypeOf 能夠判斷一個對象是否在另一個對象的原型鏈上。
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 。這個函數可以修改任何對象的 [[Prototype]] ,包括內建類型。
var anotherParent = { name: "Alex" } Object.setPrototypeOf(child, anotherParent) Object.getPrototypeOf(child) // anotherParent // 修改數組的 [[Prototype]] var a = [] Object.setPrototypeOf(a, anotherParent) a instanceof Array // false Object.getPrototypeOf(a) // anotherParent
靈活使用以上的幾個方法,我們可以非常輕松地創建原型鏈,或者在已知原型鏈中插入自定義的對象,玩法只取決于想象力。我們以此修改一下上面的模擬繼承的例子:
function Parent() {} function Child() {} Child.prototype = Object.create(Parent.prototype) Child.prototype.constructor = Child
因為 Object.create(..) 傳入的參數會作為 [[Prototype]] ,所以這里有一個有意思的小技巧。我們可以用 Object.create(null) 創建一個沒有任何屬性的對象。這個技巧適合做 proxy 對象,有點類似 Ruby 中的 BasicObject。
尷尬的私生子 __proto__說到操作 [[Prototype]] 就不得不提 __proto__ 。這個屬性是一個 getter/setter ,可以用來獲取和設置任意對象的 [[Prototype]] 。
child.__proto__ // equal to Object.getPrototypeOf(child) child.__proto__ = parent // equal to Object.setPrototypeOf(child, parent)
它本來不是 ES 的標準,無奈眾多瀏覽器早早地都實現了這個屬性,而且應用得還挺廣泛的。到了 ES6 為了向下兼容性只好接納它成為標準的一部分。這是典型的現實倒逼標準的例子。
看看 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__ 是不被推薦的用法。大部分情況下我們仍然應該用 Object.getPrototypeOf 和 Object.setPrototypeOf 。什么是少數情況,待會再講。
ES6: class 語法糖不得不說開發者世界受 OO 的影響非常之深,雖然 ES5 給了我們足夠靈活的 API ,但是:
很多人還是傾向于用 class 來組織代碼。
很多類庫、框架創造了自己的 API 來實現 class 的功能。
產生這一現象的原因有很多,但事實如此。而且如果用別人的輪子,有些事是我們無法選擇的。也許是看到了這一現象,ES6 時代終于有了 class 語法,有望統一各個類庫和框架不一致的類實現方式。來看一個例子:
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 語法是一個意思:
function User(firstName, lastName) { this.firstName = firstName this.lastName = lastName } User.prototype.fullName = function() { return "" + this.firstName + this.lastName }
ES6 并沒有改變 JavaScript 基于原型的本質,只是在此之上提供了一些語法糖。class 就是其中之一。其他的還有 extends,super 和 static 。它們大多數都可以轉換成等價的 ES5 語法。
我們來看看另一個繼承的例子:
class Child extends Parent { constructor(firstName, lastName, age) { super(firstName, lastName) this.age = age } }
其基本等價于:
function Child(firstName, lastName, age) { Parent.call(this, firstName, lastName) this.age = age } Child.prototype = Object.create(Parent.prototype) Child.constructor = Child
無疑上面的例子更加直觀,代碼組織更加清晰。這也是加入新語法的目的。不過雖然新語法的本質還是基于原型的,但新加入的概念或多或少會引起一些連帶的影響。
extends 繼承內建類的能力因為語言內部設計原因,我們沒有辦法自定義一個類來繼承 JavaScript 的內建類的。繼承類往往會有各種問題。ES6 的 extends 的最大的賣點,就是不僅可以繼承自定義類,還可以繼承 JavaScript 的內建類,比如這樣:
class MyArray extends Array { }
這種方式可以讓開發者繼承內建類的功能創造出符合自己想要的類。所有 Array 已有的屬性和方法都會對繼承類生效。這確實是個不錯的誘惑,也是繼承最大的吸引力。
但現實總是悲催的。extends 內建類會引發一些奇怪的問題,很多屬性和方法沒辦法在繼承類中正常工作。舉個例子:
var a = new Array(1, 2, 3) a.length // 3 var b = new MyArray(1, 2, 3) b.length // 0
如果說語法糖可以用 Babel.js 這種 transpiler 去編譯成 ES5 解決 ,擴充的 API 可以用 polyfill 解決,但是這種內建類的繼承機制顯然是需要瀏覽器支持的。而目前唯一支持這個特性的瀏覽器是………… Microsoft Edge 。
好在這并不是什么致命的問題。大多數此類需求都可以用封裝類去解決,無非是多寫一點 wrapper API 而已。而且個人認為封裝和組合反而是比繼承更靈活的解決方案。
super 帶來的新概念(坑?) super 在 constructor 和普通方法里的不同在 constructor 里面,super 的用法是 super(..)。它相當于一個函數,調用它等于調用父類的 constructor 。但在普通方法里面,super 的用法是 super.prop 或者 super.method()。它相當于一個指向對象的 [[Prototype]] 的屬性。這是 ES6 標準的規定。
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 對方法里調用 super(..) 也能編譯出正確的結果,但這應該是 Babel.js 的 bug ,我們不該以此得出 super(..) 也可以在非 constructor 里用的結論。
super 在子類的 constructor 里必須先于 this 調用如果寫子類的 constructor 需要操作 this ,那么 super 必須先調用!這是 ES6 的規則。所以寫子類的 constructor 時盡量把 super 寫在第一行。
class Child extends Parent { constructor() { this.xxx() // invalid super() } }super 是編譯時確定,不是運行時確定
什么意思呢?先看代碼:
class Child extends Parent { fullName() { super.fullName() } }
以上代碼中 fullName 方法的 ES5 等價代碼是:
fullName() { Parent.prototype.fullName.call(this) }
而不是
fullName() { Object.getPrototypeOf(this).fullName.call(this) }
這就是 super 編譯時確定的特性。不過為什么要這樣設計?個人理解是,函數的 this 只有在運行時才能確定。因此在運行時根據 this 的原型鏈去獲得上層方法并不太符合 class 的常規思維,在某些情況下更容易產生錯誤。比如 child.fullName.call(anotherObj) 。
super 對 static 的影響,和類的原型鏈static 相當于類方法。因為編譯時確定的特性,以下代碼中:
class Child extends Parent { static findAll() { return super.findAll() } }
findAll 的 ES5 等價代碼是:
findAll() { return Parent.findAll() }
static 貌似和原型鏈沒關系,但這不妨礙我們討論一個問題:類的原型鏈是怎樣的?我沒查到相關的資料,不過我們可以測試一下:
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]] 都是同一個函數對象,不管是內建類如 Object 還是自定義類如 Parent 。但這個函數是不能用 new 關鍵字初始化的。雖然這種設計沒有 Ruby 的對象模型那么巧妙,不過也是能夠自圓其說的。
直接定義 object 并設定 [[Prototype]]除了通過 class 和 extends 的語法設定 [[Prototype]] 之外,現在定義對象也可以直接設定 [[Prototype]] 了。這就要用到 __proto__ 屬性了。“定義對象并設置 [[Prototype]]” 是唯一建議用 __proto__ 的地方。另外,另外注意 super 只有在 method() {} 這種語法下才能用。
let parent = { method1() { .. }, method2() { .. }, } let child = { __proto__: parent, // valid method1() { return super.method1() }, // invalid method2: function() { return super.method2() }, }總結
JavaScript 的原型是很有意思的設計,從某種程度上說它是更加純粹的面向對象設計(而不是面向類的設計)。ES5 和 ES6 加入的 API 能更有效地操控原型鏈。語言層面支持的 class 也能讓忠于類設計的開發者用更加統一的方式去設計類。雖然目前 class 僅僅提供了一些基本功能。但隨著標準的進步,相信它還會擴充出更多的功能。
本文的主題是原型系統的變遷,所以并沒有涉及 getter/setter 和 defineProperty 對原型鏈的影響。想系統地學習原型,你可以去看 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__
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92343.html
摘要:因為操作符創建的對象都繼承自構造函數的屬性。繼承的實現中常用的繼承方式是組合繼承,也就是通過構造函數和原型鏈繼承同時來模擬繼承的實現。 原文發布在我的博客 我們都知道 JavaScript 是一門基于原型的語言。當我們調用一個對象本身沒有的屬性時,JavaScript 就會從對象的原型對象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型,一直找原型鏈的末端也就是 Object....
摘要:插件開發前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內優雅的實現文件分片斷點續傳。 Vue.js 插件開發 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
摘要:前端每周清單第期與模式變遷與優化界面生成作者王下邀月熊編輯徐川前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000013279448); 前端每周清單第 51 期: React Context A...
摘要:正文在年,框架的選擇并不少。特別的,通過思考這些框架分別如何處理狀態變化是很有用的。本文探索以下的數據綁定,的臟檢查的虛擬以及它與不可變數據結構之間的聯系。當狀態產生變化時,只有真正需要更新的部分才會發生改變。 譯者言 近幾年可謂是 JavaScript 的大爆炸紀元,各種框架類庫層出不窮,它們給前端帶來一個又一個的新思想。從以前我們用的 jQuery 直接操作 DOM,到 Backb...
閱讀 639·2021-08-17 10:15
閱讀 1716·2021-07-30 14:57
閱讀 1976·2019-08-30 15:55
閱讀 2814·2019-08-30 15:55
閱讀 2707·2019-08-30 15:44
閱讀 668·2019-08-30 14:13
閱讀 2384·2019-08-30 13:55
閱讀 2590·2019-08-26 13:56