成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JavaScript 原型系統(tǒng)的變遷,以及 ES6 class

chuyao / 2576人閱讀

摘要:一般我們對這種構(gòu)造函數(shù)命名都會采用,并把它稱呼為類,這不僅是為了跟的理念保持一致,也是因為的內(nèi)建類也是這種命名。由生成的對象,其是。這是標(biāo)準(zhǔn)的規(guī)定。本文的主題是原型系統(tǒng)的變遷,所以并沒有涉及和對原型鏈的影響。

概述

JavaScript 的原型系統(tǒng)是最初就有的語言設(shè)計。但隨著 ES 標(biāo)準(zhǔn)的進(jìn)化和新特性的添加。它也一直在不停進(jìn)化。這篇文章的目的就是梳理一下早期到 ES5 和現(xiàn)在 ES6,新特性的加入對原型系統(tǒng)的影響。

如果你對原型的理解還停留在 function + new 這個層面而不知道更深入的操作原型鏈的技巧,或者你想了解 ES6 class 的知識,相信本文會有所幫助。

這篇文章是我學(xué)習(xí) You Don"t Know JS 的副產(chǎn)品,推薦任何想系統(tǒng)性地學(xué)習(xí) JavaScript 的人去閱讀此書。

JavaScript 原型簡述

很多人應(yīng)該都對原型(prototype)不陌生。簡單地說,JavaScript 是基于原型的語言。當(dāng)我們調(diào)用一個對象的屬性時,如果對象沒有該屬性,JavaScript 解釋器就會從對象的原型對象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型。這種屬性查找的方式被稱為原型鏈(prototype chain)。

對象的原型是沒有公開的屬性名去訪問的(下文再談 __proto__ 屬性)。以下為了方便稱呼,我把一個對象內(nèi)部對原型的引用稱為 [[Prototype]]。

JavaScript 沒有類的概念,原型鏈的設(shè)定就是少數(shù)能夠讓多個對象共享屬性和方法,甚至模擬繼承的方式。在 ES5 以前,如果我們想設(shè)置對象的 [[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)用時,它就類似于一個構(gòu)造函數(shù),其生成的對象的 [[Prototype]] 會引用 User.prototype 。因為 User.prototype 也是一個對象,它的 [[Prototype]] 是 Object.prototype

一般我們對這種構(gòu)造函數(shù)命名都會采用 CamelCase ,并把它稱呼為“類”,這不僅是為了跟 OOP 的理念保持一致,也是因為 JavaScript 的內(nèi)建“類”也是這種命名。

SomeClass 生成的對象,其 [[Prototype]] 是 SomeClass.prototype。除了稍顯繁瑣,這套邏輯是可以自圓其說的,比如:

我們用 {..} 創(chuàng)建的對象的 [[Prototype]] 都是 Object.prototype,也是原型鏈的頂點(diǎn)。

數(shù)組的 [[Prototype]] 是 Array.prototype 。

字符串的 [[Prototype]] 是 String.prototype 。

Array.prototypeString.prototype 的 [[Prototype]] 是 Object.prototype 。

模擬繼承

模擬繼承是自定義原型鏈的典型使用場景。但如果用 new 的方式則比較麻煩。一種常見的解法是:子類的 prototype 等于父類的實例。這就涉及到定義子類的時候調(diào)用父類的構(gòu)造函數(shù)。為了避免父類的構(gòu)造函數(shù)在類定義過程中的潛在影響,我們一般會建造一個臨時類去做代替父類 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  // true
ES5: 自由地操控原型鏈

既然原型鏈本質(zhì)上只是建立對象之間的關(guān)聯(lián),那我們可不可以直接操作對象的 [[Prototype]] 呢?

在 ES5(準(zhǔn)確的說是 5.1)之前,我們沒有辦法直接獲取對象的原型,只能通過 [[Prototype]] 的 constructor。

var user = new User()
user.constructor.prototype          // User
user.hasOwnProperty("constructor")  // false

類可以通過 prototype 屬性獲取生成的對象的 [[Prototype]]。[[Prototype]] 里的 constructor 屬性又會反過來引用函數(shù)本身。因為 user 的原型是 User.prototype ,它自然也能夠通過 constructor 獲取到 User 函數(shù),進(jìn)而獲取到自己的 [[Prototype]]。比較繞是吧?

ES5.1 之后加了幾個新的 API 幫助我們操作對象的 [[Prototype]],自此以后 JavaScript 才真的有自由操控原型的能力。它們是:

Object.prototype.isPrototypeOf

Object.create

Object.getPrototypeOf

Object.setPrototypeOf

注:以上方法并不完全是 ES5.1 的,isPrototypeOf 是 ES3 就有的,setPrototypeOf 是 ES6 才有的。但它們的規(guī)范都在 ES6 中修改了一部分。

下面的例子里,Object.create 創(chuàng)建 child 對象,并把 [[Prototype]] 設(shè)置為 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 。這個函數(shù)可以修改任何對象的 [[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

靈活使用以上的幾個方法,我們可以非常輕松地創(chuàng)建原型鏈,或者在已知原型鏈中插入自定義的對象,玩法只取決于想象力。我們以此修改一下上面的模擬繼承的例子:

function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

因為 Object.create(..) 傳入的參數(shù)會作為 [[Prototype]] ,所以這里有一個有意思的小技巧。我們可以用 Object.create(null) 創(chuàng)建一個沒有任何屬性的對象。這個技巧適合做 proxy 對象,有點(diǎn)類似 Ruby 中的 BasicObject。

尷尬的私生子 __proto__

說到操作 [[Prototype]] 就不得不提 __proto__ 。這個屬性是一個 getter/setter ,可以用來獲取和設(shè)置任意對象的 [[Prototype]] 。

child.__proto__           // equal to Object.getPrototypeOf(child)
child.__proto__ = parent  // equal to Object.setPrototypeOf(child, parent)

它本來不是 ES 的標(biāo)準(zhǔn),無奈眾多瀏覽器早早地都實現(xiàn)了這個屬性,而且應(yīng)用得還挺廣泛的。到了 ES6 為了向下兼容性只好接納它成為標(biāo)準(zhǔn)的一部分。這是典型的現(xiàn)實倒逼標(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.getPrototypeOfObject.setPrototypeOf 。什么是少數(shù)情況,待會再講。

ES6: class 語法糖

不得不說開發(fā)者世界受 OO 的影響非常之深,雖然 ES5 給了我們足夠靈活的 API ,但是:

很多人還是傾向于用 class 來組織代碼。

很多類庫、框架創(chuàng)造了自己的 API 來實現(xiàn) class 的功能。

產(chǎn)生這一現(xiàn)象的原因有很多,但事實如此。而且如果用別人的輪子,有些事是我們無法選擇的。也許是看到了這一現(xiàn)象,ES6 時代終于有了 class 語法,有望統(tǒng)一各個類庫和框架不一致的類實現(xiàn)方式。來看一個例子:

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 基于原型的本質(zhì),只是在此之上提供了一些語法糖。class 就是其中之一。其他的還有 extends,superstatic 。它們大多數(shù)都可以轉(zhuǎn)換成等價的 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

無疑上面的例子更加直觀,代碼組織更加清晰。這也是加入新語法的目的。不過雖然新語法的本質(zhì)還是基于原型的,但新加入的概念或多或少會引起一些連帶的影響。

extends 繼承內(nèi)建類的能力

因為語言內(nèi)部設(shè)計原因,我們沒有辦法自定義一個類來繼承 JavaScript 的內(nèi)建類的。繼承類往往會有各種問題。ES6 的 extends 的最大的賣點(diǎn),就是不僅可以繼承自定義類,還可以繼承 JavaScript 的內(nèi)建類,比如這樣:

class MyArray extends Array {
}

這種方式可以讓開發(fā)者繼承內(nèi)建類的功能創(chuàng)造出符合自己想要的類。所有 Array 已有的屬性和方法都會對繼承類生效。這確實是個不錯的誘惑,也是繼承最大的吸引力。

但現(xiàn)實總是悲催的。extends 內(nèi)建類會引發(fā)一些奇怪的問題,很多屬性和方法沒辦法在繼承類中正常工作。舉個例子:

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ī)制顯然是需要瀏覽器支持的。而目前唯一支持這個特性的瀏覽器是………… Microsoft Edge 。

好在這并不是什么致命的問題。大多數(shù)此類需求都可以用封裝類去解決,無非是多寫一點(diǎn) wrapper API 而已。而且個人認(rèn)為封裝和組合反而是比繼承更靈活的解決方案。

super 帶來的新概念(坑?) super 在 constructor 和普通方法里的不同

在 constructor 里面,super 的用法是 super(..)。它相當(dāng)于一個函數(shù),調(diào)用它等于調(diào)用父類的 constructor 。但在普通方法里面,super 的用法是 super.prop 或者 super.method()。它相當(dāng)于一個指向?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 對方法里調(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 時盡量把 super 寫在第一行。

class Child extends Parent {
  constructor() {
    this.xxx()  // invalid
    super()
  }
}
super 是編譯時確定,不是運(yùn)行時確定

什么意思呢?先看代碼:

class Child extends Parent {
  fullName() {
    super.fullName()
  }
}

以上代碼中 fullName 方法的 ES5 等價代碼是:

fullName() {
  Parent.prototype.fullName.call(this)
}

而不是

fullName() {
  Object.getPrototypeOf(this).fullName.call(this)
}

這就是 super 編譯時確定的特性。不過為什么要這樣設(shè)計?個人理解是,函數(shù)的 this 只有在運(yùn)行時才能確定。因此在運(yùn)行時根據(jù) this 的原型鏈去獲得上層方法并不太符合 class 的常規(guī)思維,在某些情況下更容易產(chǎn)生錯誤。比如 child.fullName.call(anotherObj) 。

super 對 static 的影響,和類的原型鏈

static 相當(dāng)于類方法。因為編譯時確定的特性,以下代碼中:

class Child extends Parent {
  static findAll() {
    return super.findAll()
  }
}

findAll 的 ES5 等價代碼是:

findAll() {
  return Parent.findAll()
}

static 貌似和原型鏈沒關(guān)系,但這不妨礙我們討論一個問題:類的原型鏈?zhǔn)窃鯓拥??我沒查到相關(guān)的資料,不過我們可以測試一下:

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]] 都是同一個函數(shù)對象,不管是內(nèi)建類如 Object 還是自定義類如 Parent 。但這個函數(shù)是不能用 new 關(guān)鍵字初始化的。雖然這種設(shè)計沒有 Ruby 的對象模型那么巧妙,不過也是能夠自圓其說的。

直接定義 object 并設(shè)定 [[Prototype]]

除了通過 classextends 的語法設(shè)定 [[Prototype]] 之外,現(xiàn)在定義對象也可以直接設(shè)定 [[Prototype]] 了。這就要用到 __proto__ 屬性了?!岸x對象并設(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è)計,從某種程度上說它是更加純粹的面向?qū)ο笤O(shè)計(而不是面向類的設(shè)計)。ES5 和 ES6 加入的 API 能更有效地操控原型鏈。語言層面支持的 class 也能讓忠于類設(shè)計的開發(fā)者用更加統(tǒng)一的方式去設(shè)計類。雖然目前 class 僅僅提供了一些基本功能。但隨著標(biāo)準(zhǔn)的進(jìn)步,相信它還會擴(kuò)充出更多的功能。

本文的主題是原型系統(tǒng)的變遷,所以并沒有涉及 getter/setter 和 defineProperty 對原型鏈的影響。想系統(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)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/92343.html

相關(guān)文章

  • JavaScript類和繼承

    摘要:因為操作符創(chuàng)建的對象都繼承自構(gòu)造函數(shù)的屬性。繼承的實現(xiàn)中常用的繼承方式是組合繼承,也就是通過構(gòu)造函數(shù)和原型鏈繼承同時來模擬繼承的實現(xiàn)。 原文發(fā)布在我的博客 我們都知道 JavaScript 是一門基于原型的語言。當(dāng)我們調(diào)用一個對象本身沒有的屬性時,JavaScript 就會從對象的原型對象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型,一直找原型鏈的末端也就是 Object....

    rottengeek 評論0 收藏0
  • JavaScript - 收藏集 - 掘金

    摘要:插件開發(fā)前端掘金作者原文地址譯者插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實現(xiàn)文件分片斷點(diǎn)續(xù)傳。 Vue.js 插件開發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡單的方式。插....

    izhuhaodev 評論0 收藏0
  • 前端每周清單第 51 期: React Context API 與模式變遷, Webpack 與 W

    摘要:前端每周清單第期與模式變遷與優(yōu)化界面生成作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實踐深度閱讀開源項目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000013279448); 前端每周清單第 51 期: React Context A...

    Jiavan 評論0 收藏0
  • 【譯】JavaScript 框架探索與變遷(上)

    摘要:正文在年,框架的選擇并不少。特別的,通過思考這些框架分別如何處理狀態(tài)變化是很有用的。本文探索以下的數(shù)據(jù)綁定,的臟檢查的虛擬以及它與不可變數(shù)據(jù)結(jié)構(gòu)之間的聯(lián)系。當(dāng)狀態(tài)產(chǎn)生變化時,只有真正需要更新的部分才會發(fā)生改變。 譯者言 近幾年可謂是 JavaScript 的大爆炸紀(jì)元,各種框架類庫層出不窮,它們給前端帶來一個又一個的新思想。從以前我們用的 jQuery 直接操作 DOM,到 Backb...

    Jaden 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<