對象、原型鏈、類、繼承【下】

概述

上篇文章中,咱們使用ES5經過構造函數和原型對象實現了「類」,經過原型鏈實現了「類」的繼承。在ES6中,新增classextend實現了類和繼承,提供了更接近傳統語言的寫法。javascript

class

和大多數面向對象的語言不一樣,JavaScript 在誕生之初並不支持類,也沒有把類繼承做爲建立類似或關聯的對象的主要的定義方式。因此從ES1ES5這段時期,不少庫都建立了一些工具,讓JavaScript看起來也能支持類。儘管一些JavaScript開發者仍強烈主張該語言不須要類,但在流行庫中實現類已成趨勢,ES6也順勢將其引入。但ES6 中的類和其餘語言相比並非徹底等同的,目的是爲了和JavaScript的動態特性相配合。java

定義

經過class關鍵字,能夠定義類。能夠把class看作一個語法糖,一個在ES5中必須很是複雜才能完成的實現的封裝。它使得定義一個類更加清晰明瞭,更符合面向對象編程的語法。git

對比ES5

咱們來對比一下:es6

// es5
function Person5 (name) {
    this.age = 12
    this.name = name
    this.sayAge = function () {
        return this.age
    }
}
Person5.prototype.sayName = function () {
    return this.name
}
let p1 = new Person5('zhu')
p1.age // 12
p1.sayName() // 'zhu'

// es6
class Person6 {
    constructor (name) {
        this.age = 12
        this.name = name
        this.sayAge = function () {
            return this.age
        }
    }
    sayName () {
        return this.name
    }
}

let p2 = new Person6('zhu')
p2.age // 12
p2.sayName() // 'zhu'
複製代碼

類的原型對象的方法(sayName),直接定義在類上便可。類的實例屬性(name)在constructor方法裏面定義。github

二者相比,ES5更能說請ECMAScript經過prototype實現類的原理,ES6寫法更加清晰規範。 而生成實例的方法仍是一致的:經過new命令。由於,class只是定義類的語法糖。express

原型對象的屬性

至於類原型對象的屬性的定義,目前還在提案階段編程

// es5
function Person5 () {}
Person5.prototype.shareSex = 'man'
let p1 = new Person5()
p1.shareSex // 'man'
// es6
class Person6 {
    shareSex = 'man'
}
let p2 = new Person6()
p2.shareSex // 'man'
複製代碼

constructor

類的constructor方法的行爲模式徹底與ES5的構造函數同樣(關於構造函數能夠參考{% post_link JavaScript高級程序設計第三版 %} 第6.2.2章節)。若是未定義,會默認添加。如下兩個定義是等效的。數組

class Person {}
class Person {
    constructor () {
        return this
    }
}
複製代碼

表達式

上面的例子中,類的定義方式是聲明式定義。與函數類似,類也有表達式定義的形式。瀏覽器

let Person = class {}
複製代碼

雖然使用了聲明變量,可是類表達式並不會提高。因此,聲明式聲明和表達式式聲明除了寫法不一樣,徹底等價。安全

若是兩種形式同時使用,聲明式定義的名稱可做爲內部名稱使用,指向類自己。但不能在外部使用,會報錯。

let PersonMe = class Me {
    constructor () {
        Me.age = 12 
    }
    sayAge () {
        return Me.age
    }
}
let p2 = new PersonMe()
p2.age // undefined
PersonMe.age // 12
p2.sayAge() // 12
Me.name // Uncaught ReferenceError: Me is not defined
PersonMe.name // Me
複製代碼

咱們看到PersonMe.name的值是Me,而不是PersonMe。由此可知,變量PersonMe只是存儲了一個執行Me這個類的指針。

而類名之因此能夠在內部使用,是由於具名錶達式實際是這樣的:

let PersonMe = (function() {
    const Me = function() {
        Me.age = 12
    }
    Me.prototype.sayAge = function () {
        return Me.age
    }
    return Me
})()
複製代碼

也可使用類表達式當即調用,以建立單例。

let p1 = new class {
    constructor (name) {
        this.name = name
    }
    sayName () {
        return this.name
    }
}('zhu')
複製代碼

一級公民

在編程中,能被當作值來使用的就稱爲一級公民(first-class citizen)。這意味着它能作函數的參數、返回值、給變量賦值等。在ECMAScript中,函數是一級公民;在ES6中,類一樣也是一級公民。

不可在內部重寫類名

在類的內部,類名是使用const聲明的,因此不能在類的內部重寫類名。可是在類的外部能夠,由於不管是聲明仍是表達式的形式,在定義類的上下文中,函數名都只是存儲指向類對象的指針。

區別

雖然咱們說class是語法糖,可是其某些地方表現與ES5中也是有些區別的。

new.target

new是從構造函數生成實例對象的命令。類必須使用new調用,不然會報錯,這點與ES5中的構造函數不一樣。

// es5
function Person5 (name) {
    return name
}
Person5('zhu') // zhu
// es6
class Person6 {
    constructor (name) {
        return name
    }
}
Person6('zhu') // Uncaught TypeError: Class constructor Person6 cannot be invoked without 'new'
複製代碼

而這正是經過new命令在ES6中新增的target屬性實現的。該屬性通常用在構造函數之中,返回new命令做用於的那個構造函數。若是構造函數不是經過new命令調用的,new.target會返回undefined,反之會返回做用的類。

class Person6 {
    constructor () {
        console.log(new.target)
    }
}

Person6() // undefined
new Person6() // Person6
複製代碼

值得注意的是,子類繼承父類時,new.target會返回子類。

class Father {
    constructor () {
        console.log(new.target)
    }
}
class Son extends Father {}
new Son() // Son
複製代碼

最後,咱們使用new.targetES5中模擬一下ES6class的行爲。

function Person5 () {
    if(new.target === undefined) {
        throw new TypeError("Class constructor Person6 cannot be invoked without 'new'")
    }
    console.log('success,', new.target === Person5)
}

Person5() // Uncaught TypeError: Class constructor Person6 cannot be invoked without 'new'
new Person5() // success, true
複製代碼

類的方法不可枚舉

ES6中,在類上定義的方法,都是不可枚舉的(non-enumerable)。在ES5中是能夠的。

// es5
function Person5 (name) {
    this.age = 12
    this.name = name
}
Person5.prototype.sayName = function () {
    return this.name
}

// es6
class Person6 {
    constructor (name) {
        this.age = 12
        this.name = name
    }
    sayName () {
        return this.name
    }
}

Object.getOwnPropertyDescriptor(Person5.prototype, 'sayName').enumerable // true
Object.getOwnPropertyDescriptor(Person6.prototype, 'sayName').enumerable // false
Object.keys(Person5.prototype) // ['sayName']
Object.keys(Person6.prototype) // []
Object.getOwnPropertyNames(Person5.prototype) // ["constructor", "sayName"]
Object.getOwnPropertyNames(Person6.prototype) // ["constructor", "sayName"]
複製代碼

不存在變量提高

函數能夠在當前做用域的任意位置定義,在任意位置調用。類不是函數,不存在變量提高。

// es5
new Person5()
function Person5 () {}
// es6
new Person6() // Uncaught ReferenceError: Person6 is not defined
class Person6 {}
複製代碼

內部方法不是構造函數

類的靜態方法、實例的方法內部都沒有[[Construct]]屬性,也沒有原型對象(沒有prototype屬性)。所以使用new來調用它們會拋出錯誤。

class Person6 {
    sayHi () {
        return 'hi'
    }
}
new Person6.sayHi // Uncaught TypeError: Person6.sayHi is not a constructor

Person6.prototype.sayHi.prototype // undefined
複製代碼

一樣的,箭頭函數(() => {}})也同樣。

let Foo = () => {}
new Foo // Uncaught TypeError: Person6.sayHi is not a constructor
複製代碼

這種不是構造函數的函數,在ES5中,只有內置對象的方法屬於這種狀況。

Array.prototype.concat.prototype // undefined
複製代碼

改進

除了區別,class命令也有一些對ES5構造函數的改進。好比,寫法的改變,更加靈活、規範等等。

嚴格模式

在類和模塊的內部,默認開啓了嚴格模式,也就是默認使用了use strict

動態方法名

ES6中,方法名能夠動態命名。訪問器屬性也可使用動態命名。

let methodName1 = 'sayName'
let methodName2 = 'sayAge'
class Person {
    constructor (name) {
        this.name = name 
    }
    [methodName1] () {
        return this.name
    }
    get [methodName2] () {
        return 24
    }
}
let p1 = new Person('zhu')
p1.sayName() // zhu
複製代碼

訪問器屬性

ES5中,若是要將構造函數的實例屬性設置成訪問器屬性,你要這樣作:

function Person5 () {
    this._age = 12
    Object.defineProperty(this, 'age', {
        get: function () {
            console.log('get')
            return this._age
        },
        set: function (val) {
            console.log('set')
            this._age = val
        }
    })
}
let p1 = new Person5()
p1.age // get 12
p1.age = 15 // set
p1.age // get 15
複製代碼

ES6中咱們有了更方便的寫法:

class Person6 {
    constructor () {
        this._age = 12
    }
    get age () {
        console.log('get')
        return this._age
    }
    set age (val) {
        console.log('set')
        this._age = val
    }
}
let p2 = new Person6()
p2.age // get 12
p2.age = 15 // set
p2.age // get 15
複製代碼

靜態屬性和靜態方法

類的靜態屬性和靜態方法是定義在類上的,也能夠說是定義在構造函數的。它們不能被實例對象繼承,可是能夠被子類繼承。須要注意的是,靜態屬性若是是引用類型,子類繼承的是指針。 在ES6中,除了constructor方法,在類的其餘方法名前面加上static關鍵字,就表示這是一個靜態方法。

// es5
function Person5 () {}
Person5.age = 12
Person5.sayAge = function () {
    return this.age
} 
Person5.age // 12
Person5.sayAge() // 12
let p1 = new Person5()
p1.age // undefined
p1.sayAge // undefined
// 繼承
Sub5.__proto__ = Person5
Sub5.age // 12
Sub5.sayAge() // 12
// es6 
class Person6 {
    static sayAge () {
        return this.age
    }
}
Person6.age = 12
Person6.age // 12
Person6.sayAge() // 12
let p2 = new Person5()
p2.age // undefined
p2.sayAge // undefined
// 繼承
class Sub6 extends Person6 {}
Sub6.age // 12
Sub6.sayAge() // 12
複製代碼

須要注意的是,靜態方法裏面的this關鍵字,指向的是類,而不是實例。因此爲了不混淆,建議在靜態方法中,直接使用類名。

class Person1 {
    constructor (name) {
        this.name = name
    }
    static getName () {
        return this.name
    }
    getName () {
        return this.name
    }
}

let p1 = new Person1('zhu')
p1.getName() // 'zhu'
Person1.getName() // 'Person1'
class Person2 {
    constructor (name) {
        this.name = name
    }
    static getName () {
        return Person2.name
    }
    getName () {
        return this.name
    }
}
let p2 = new Person2('zhu')
p2.getName() // 'zhu'
Person2.getName() // 'Person2'
複製代碼

從上面的實例中咱們能夠看到,靜態方法與非靜態方法是能夠重名的。

ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。因此,目前只能在Class外部定義(Person6.age = 12)。 可是,如今已經有了相應的提案

class Person6 {
   static age = 12
   static sayAge () {
       return this.age
   }
}
複製代碼

私有方法和私有屬性

私有屬性其實就在類中提早聲明的,只能在類內部使用的屬性。以下示例:

class PersonNext {
    static x; // 靜態屬性;定義在類上,會被子類繼承
    public y; // 實例屬性。通常簡寫爲 [y;],忽略public關鍵字,定義在實例上。
    #z; // 私有屬性。相似於其餘語言中的private。只能在類內部使用
}

複製代碼

因爲此寫法還在提案階段,本文暫不詳細說明,有興趣能夠關注提案的進度

其餘

this 的指向

Class上實例方法中的this,默認指向實例自己。可是使用解構賦值後,在函數指向時,做用域指向發生了改變,就有可能引發報錯。雖然說有解決的方法,可是仍是儘可能避免使用這種方式吧。

class Person6 {
    constructor () {
        this.name = 'zhu'
    }
    sayName () {
        return this.name
    }
}
let p1 = new Person6()
p1.sayName() // zhu
let { sayName } = p1
sayName() // Uncaught TypeError: Cannot read property 'name' of undefined
sayName.call(p1) // zhu
複製代碼

babel

最後,咱們看一下classbabel中如何轉換成ES6的

let methodName = 'sayName'
class Person {
  	constructor (name) {
    	this.name = name
      	this.age = 46
    }
	static create (name) {
    	return new Person(name)
    }
  	sayAge () {
    	return this.age
    }
  	[methodName] () {
    	return this.name
    }
}
複製代碼
"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var methodName = "sayName";
var Person = (function() {
  function Person(name) {
    _classCallCheck(this, Person);
    this.name = name;
    this.age = 46;
  }
  Person.create = function create(name) {
    return new Person(name);
  };
  Person.prototype.sayAge = function sayAge() {
    return this.age;
  };
  Person.prototype[methodName] = function() {
    return this.name;
  };
  return Person;
})();
複製代碼

_classCallCheck方法算是new.target的polyfill。

繼承

class繼承主要就是添加了extends關鍵字,相比與class,extends不只僅是語法糖,還實現了許多ES5沒法實現的功能。也就是說,extends是沒法徹底降級到ES5的。好比,內置對象的繼承

extends

class 能夠經過extends關鍵字實現繼承,這比ES5的經過修改原型鏈實現繼承,要清晰和方便不少。 咱們先來回顧下ES5的實現:

function Father5 (name) {
    this.name = name
    this.age = 46
}
Father5.prototype.sayName = function () {
    return this.name
}
Father5.prototype.sayAge = function () {
    return this.age
}

Father5.create = function (name) {
    return new this(name)
}

function Son5 (name) {
    Father5.call(this, name)
}
Son5.prototype = Object.create(Father5.prototype, {
    constructor: {
        value: Son5,
        enumerable: true,
        writable: true,
        configurable: true
    }
})
Son5.__proto__ = Father5

Son5.prototype.setAge = function (age) {
    this.age = age
}

var s1 = Son5.create('zhu')
s1.constructor // Son5
s1.sayName() // 'zhu'
s1.sayAge() // 46
s1.setAge(12)
s1.sayAge() // 12
複製代碼

而後,咱們看下classextends 如何實現:

let Father6 = class Me {
    constructor (name) {
        this.name = name
        this.age = 46
    }
    static create (name) {
    	return new Me(name)
    }
    sayName () {
        return this.name
    }
    sayAge () {
        return this.age
    }
}

let Son6 = class Me extends Father6 {
    constructor (name) {
        super(name)
    }
    setAge (age) {
        this.age = age
    }
}

let s2 = Son6.create('sang')
s2.constructor // Son6
s2.sayName() // 'sang'
s2.sayAge() // 46
s2.setAge(13)
s2.sayAge() // 13
複製代碼

咱們看到extendssuper(name)作了三件事:實例屬性繼承,原型對象繼承,靜態屬性繼承。接下來,咱們就來講說super

super

在子類中,若是定義了constructor,則必須在第一行調用super。由於super對子類的this進行了封裝,使之繼承了父類的屬性和方法。 若是在super調用以前使用this,會報錯。

class Son extends Father {
    constructor (name) {
        this.name = name // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
        super(name)
        this.name = name // 正常執行
    }
}
複製代碼

若是沒有定義constructor,則會默認添加。

class Son extends Father {}
// 等同於
class Son extends Father {
    constructor (..arg) {
        super(..arg)
    }
}
複製代碼

super關鍵字必須做爲一個函數或者一個對象使用,若是做爲值使用會報錯。

class Son extends Father{
    constructor (name) {
        super(name)
		console.log(super) // Uncaught SyntaxError: 'super' keyword unexpected here
    }
}
複製代碼

做爲函數調用時,只能在子類的constructor函數中,不然也會報錯。

做爲對象使用時,在普通方法中,指向的是原父類的原型對象;在靜態方法中,指向的是父類自己。

最後,因爲對象老是繼承其餘對象的,因此能夠在任意一個對象中,使用super關鍵字

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]
複製代碼

惟一在constructor中能夠不調用super的狀況是,constructor顯式的返回了一個對象。 不過,這種寫法好像沒什麼意義。

沒有繼承

ECMAScript中,咱們會常用字面量去「構造」一個基本類型數據。這實際上是使用new命令構造一個實例的語法糖。這每每讓咱們誤覺得在ECMAScript中,一切函數都是構造函數,而一切對象都是這些構造函數的實例,而ECMAScript也是一門面向對象的語言。

// 引用類型
var obj = {} // var obj = new Object()
var arr = [] // var arr = new Array()
// 值類型
var str = "" // var strObj = new String();var str = strObj.valueOf() 
複製代碼

ECMAScript並非純粹的面嚮對象語言,它裏面也有函數式編程的東西。因此,並非每一個函數都有原型對象,都有constructor

好比原生構造函數的原型對象上面的方法(如Array.prototype.concatNumber.prototype.toFixed)都是沒有prototype屬性的。還有,箭頭函數也是沒有prototype屬性的。因此,這些函數是不能是用new命令的,若是用了會拋錯。

new Array.prototype.concat() // Uncaught TypeError: Array.prototype.concat is not a constructor
複製代碼

這些沒有prototype屬性的方法,是函數式編程的實現,看起來也更純粹。使用這些方法時,也建議使用lambda的鏈式語法。

表達式繼承

extends後面能接受任意類型的表達式,這帶來了巨大的可能性。例如,動態的決定父類。

class FatherA {}
class FatherB {}
const type = 'A'
function select (type) {
    return type === 'A' ? FatherA : FatehrB 
}
class Son extends select('A') {
    constructor () {
        super()
    }
}
Object.getPrototypeOf(Son) === FatherA // true
複製代碼

若是,想要一個子類同時繼承多個對象的方法呢?咱們也可使用mixin

Mixin

Mixin 指的是多個對象合成一個新的對象,新對象具備各個組成成員的接口。下面示例,mixin的返回對象的原型對象,是傳入的幾個對象的原型對象的合成。

const objA = {
    sayA() {
        return 'A'
    }
}
const objB = {
    sayB() {
        return 'B'
    }
}
const objC = {
    sayC() {
        return 'C'
    }
}
function mixin (...args) {
    const base = function () {}
    Object.assign(base.prototype, ...args)
    return base
}
class Son extends mixin(objA, objB, objC) {}
let s1 = new Son()
s1.sayA() // 'A'
s1.sayB() // 'B'
s1.sayC() // 'C'
複製代碼

咱們更進一步,將實例對象也合成進去。

function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix.prototype, mixin); // 拷貝實例屬性
    copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷貝原型屬性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== "constructor"
      && key !== "prototype"
      && key !== "name"
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}
複製代碼

內置對象的繼承

ES5及以前,沒法經過繼承機制來繼承內置對象的某些特性。咱們以試圖建立一個特殊數組爲例:

// es5
// Array 的特性
var colors = []
colors[0] = 'red'
// length 跟着改變
colors.length // 1
// 改變數組的length
colors.length = 0
colors[0] // undefined

// 試圖使用ES5的方式繼承
function MyArray () {
    Array.apply(this)
}
MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
})

var colors = new MyArray()
colors[0] = 'red'
// length 沒有跟着改變
colors.length // 0
// 改變數組的length
colors.length = 0
colors[0] // 'red'
複製代碼

結果並不盡如人意,咱們繼續使用ES6的繼承:

class MyArray extends Array {}
let colors = new MyArray()
colors[0] = 'red'
colors.length // 1
colors.length = 0
colors[0] // undefined
複製代碼

與咱們的預期徹底一致。因此,ES5ES6中對內置對象的繼承仍是有區別的。

ES5中,this的值是被MyArray函數建立的,也就是說this的值實際上是MyArray的實例,而後Array.apply(this)被調用,this上面又被添加了Array上面一些附加的方法和屬性,而內置的屬性和方法並無被添加到this上。

而在ES6中,this的值會先被Array建立(super()),而後纔會把MyArray的上面的附加屬性和方法添加上去。

基於此,咱們能夠經過繼承內置對象實現更多更利於咱們本身使用的「超級內置對象」。

Symbol.species

類的Symbol.species屬性,指向一個構造函數。建立衍生對象時,會使用該屬性。

下面示例中,aMyArray的實例,而bc便是所謂的衍生對象。

class MyArray extends Array {
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

a.constructor // MyArray
b.constructor // MyArray
c.constructor // MyArray
複製代碼

默認的Symbol.species的值以下:

static get [Symbol.species]() {
  return this;
}
複製代碼

咱們能夠試着改變它。

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray();
const b = a.map(x => x);

a.constructor // MyArray
b.constructor // Array
複製代碼

咱們看到衍生對象的構造函數執行發生了變化。

注意

繼承Object的子類,有一個行爲差別

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
o.attr  // undefined
複製代碼

上面代碼中,NewObj繼承了Object,可是沒法經過super方法向父類Object傳參。這是由於 ES6 改變了Object構造函數的行爲,一旦發現Object方法不是經過new Object()這種形式調用,ES6 規定Object構造函數會忽略參數。

babel

咱們將如下ES6的代碼,在babel中轉換爲ES5的代碼。

let Father6 = class Me {
    constructor (name) {
        this.name = name
        this.age = 46
    }
    static create (name) {
    	return new Me(name)
    }
    sayName () {
        return this.name
    }
    sayAge () {
        return this.age
    }
}
let Son6 = class Me extends Father6 {
    constructor (name) {
        super(name)
    }
    setAge (age) {
        this.age = age
    }
}
複製代碼

轉換後的代碼:

"use strict";

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return call && (typeof call === "object" || typeof call === "function")
    ? call
    : self;
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError(
      "Super expression must either be null or a function, not " +
        typeof superClass
    );
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Father6 = (function() {
  function Me(name) {
    _classCallCheck(this, Me);

    this.name = name;
    this.age = 46;
  }

  Me.create = function create(name) {
    return new Me(name);
  };

  Me.prototype.sayName = function sayName() {
    return this.name;
  };

  Me.prototype.sayAge = function sayAge() {
    return this.age;
  };

  return Me;
})();

var Son6 = (function(_Father) {
  _inherits(Me, _Father);

  function Me(name) {
    _classCallCheck(this, Me);

    return _possibleConstructorReturn(this, _Father.call(this, name));
  }

  Me.prototype.setAge = function setAge(age) {
    this.age = age;
  };

  return Me;
})(Father6);
複製代碼

babel定義了三個有趣的方法:

  1. _classCallCheck用於判斷類是否被new命令符調用,是new.target的polyfill;
  2. _inherits用於子類繼承父類的原型對象和靜態方法,
  3. _possibleConstructorReturn用於繼承實例屬性。這個方法裏面有個頗有意思的判斷,若是構造函數的返回是object或者function,就把這個返回值做爲子類的實例,反之,返回子類的實例。這是爲了降級解決ES5中沒法繼承內置對象的問題,由於內置對象默認都會返回對應的實例,而咱們自定義的構造函數通常是不會寫返回值的。 這樣咱們在ES5中若是要繼承內置對象,就不能給子類添加自定義的方法和屬性了,由於返回的是內置對象的實例。

常見問題

__proto__的指向

class Father extends Function {}
class Son extends Father {}
let s1 = new Son()
複製代碼

先說幾個定義:

  1. 實例對象的__proto__屬性指向類的原型對象。
  2. 類的__proto__屬性指向它的父類, prototype指向它的原型對象。
  3. 子類的原型對象的__proto__指向父類的原型對象。
  4. 對象必定是實例,實例不必定是對象。

咱們開始驗證:

  • s1Son 的實例對象。
  • SonFather 的子類。
s1.__proto__ === Son.prototype // true
Son.__proto__ === Father  // ture
Son.prototype.__proto__ === Father.prototype // true 
複製代碼

第1,2,3條都獲得了驗證。

咱們繼續順着原型鏈往下走:

  • FatherFunction 的子類
Father.__proto__ === Function // true
Father.prototype.__proto__ === Function.prototype // true
複製代碼

第2,3條都獲得了驗證。

咱們知道全部的函數或者類都是原先構造函數Function的實例。因此:

Function.__proto__ === Function.prototype // true
typeof Function.prototype // 'function'
複製代碼

第1,4條獲得了印證。同時,Function.prototype是函數,咱們也能夠說Function.prototype是全部函數的父類。

咱們知道全部對象都是原先構造函數Object的實例,因此:

Function.prototype.__proto__ === Object.prototype // true
複製代碼

全部的原型對象都繼承自Object.prototype。因此:

Object.prototype.__proto__ === null // true
複製代碼

雞生蛋,蛋生雞

Object instanceof Function // true
Function instanceof Object // true
複製代碼

咱們看一下instanceof的定義:instanceof運算符用於測試構造函數的prototype屬性是否出如今對象的原型鏈中的任何位置

Object自己是構造函數,繼承了Function.prototype;

Object.__proto__ === Function.prototype
複製代碼

Function也是對象,繼承了Object.prototype

Function.__proto__.__proto__ === Object.prototype
複製代碼

因此誰先存在的呢?

// 肯定Object.prototype是原型鏈的頂端
Object.prototype.__proto__ === null // true
// 肯定Function.prototype繼承自Object.prototype
Function.prototype.__proto__ === Object.prototype // true
// 肯定全部的原生構造函數繼承自Function.prototype
Function.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
String.__proto__ === Function.prototype // true
Number.__proto__ === Function.prototype // true
Boolean.__proto__ === Function.prototype // true
複製代碼

Object.prototype只是一個指針,它指向一個對象(就叫它protoObj吧)。protoObj是瀏覽器最早建立的對象,這個時候Object.prototype尚未指向它,由於Object尚未被建立。而後根據protoObj建立了另外一個即便函數又是對象的funProConstructor,也就是Function.prototype指向的內存地址(是的,Function.prototype也是一個指針),可是如今它們尚未創建關係,Function.prototype尚未指向funProConstructor。再而後,瀏覽器使用funProConstructor構造函數,建立出了咱們熟悉的原生構造函數ObjectFunction等等,因此這些原生構造函數的__proto__屬性指向了它們的父類Function.prototype,而這時候,建立出來的ObjectFunction上面的Object.prototypeFunction.prototype也分別指向了protoObjfunProConstructor。自此,瀏覽器內部原型相關的內容初始化完畢。

咱們將上面的描述整理以下:

瀏覽器內部實現

解開全部疑惑的關鍵都在這麼一句話:Function.prototype是個不一樣於通常函數(對象)的函數(對象)。

gettersetter

由於__proto__並非標準的一部分,因此不建議使用。若是要在ES6中讀取和修改原型,推薦使用:Object.getPrototypeOfObject.setPrototypeOf

總結

ES6的類讓JS中的繼承變得更簡單,所以對於你已從其餘語言學習到的類知識,你無須將其丟棄。ES6的類起初是做爲ES5傳統繼承模型的語法糖,但添加了許多特性來減少錯誤。

ES6的類配合原型繼承來工做,在類的原型上定義了非靜態的方法,而靜態的方法最終則被綁定在類構造器自身上。類的全部方法初始都是不可枚舉的,這更契合了內置對象的行爲, 後者的方法默認狀況下一般都不可枚舉。此外,類構造器被調用時不能缺乏new ,確保了不能意外地將類做爲函數來調用。

基於類的繼承容許你從另外一個類、函數或表達式上派生新的類。這種能力意味着你能夠調用一個函數來判斷須要繼承的正確基類,也容許你使用混入或其餘不一樣的組合模式來建立一個新類。新的繼承方式讓繼承內置對象(例如數組)也變爲可能,而且其工做符合預期。

你能夠在類構造器內部使用new.target ,以便根據類如何被調用來作出不一樣的行爲。最經常使用的就是建立一個抽象基類,直接實例化它會拋出錯誤,但它仍然容許被其餘類所繼承。

總之,類是JS的一項新特性,它提供了更簡潔的語法與更好的功能,經過安全一致的方式來自定義一個對象類型。

參考

  1. Class 的基本語法
  2. Class 的繼承
  3. understandinges6
  4. 從__proto__和prototype來深刻理解JS對象和原型鏈
  5. javascript-functions-without-prototype
相關文章
相關標籤/搜索