javascript 類的封裝和類的繼承及原型和原型鏈詳解

本文主要講ES5、ES6中類的定義、封裝和類的繼承,以及一些注意事項(xiàng),文中除了參考引用一些資料外,也加入了很多自己的理解,如有錯(cuò)誤,歡迎讀者糾正。

javascript 類的定義

ES5 中的類

在 ES5 中,其實(shí)是沒有類的概念的,原因是因?yàn)?js 的作者當(dāng)初在設(shè)計(jì)這門語言的時(shí)候認(rèn)為,一旦像 C++和 Java 一樣引入了類,js 就是一種完整的面向?qū)ο缶幊陶Z言了,這好像有點(diǎn)太正式了,而其增加了初學(xué)者的入門難度。他當(dāng)時(shí)認(rèn)為,沒必要將 js 設(shè)計(jì)得很復(fù)雜,這種語言只要能夠完成一些簡(jiǎn)單操作就夠了,比如判斷用戶有沒有填寫表單。因此,有了一些模擬定義類的方法出現(xiàn)。

  1. 構(gòu)造函數(shù)法

這個(gè)方法相信稍微有些 js 基礎(chǔ)的人都能知道。

function People(name) {
  this.name = name
}
var jack = new People('jack')
People.prototype.constructor === People // true
jack.constructor === People // true
jack.__proto__ === People.prototype // true

但是這種方法的 protype 上的屬性只能在外面定義,不夠語義化,也挺麻煩。

People.prototype.species = 'human'
  1. Object.create()法

為了解決上述的缺點(diǎn),ES5 有了 Object.create()的方法來直接傳入 prototype。

var People={
  species='human'
}
var jack=Object.create(People)
jack.name='jack'

顯而易見,這種方法的問題是定義實(shí)例屬性比較麻煩,而且也不能實(shí)現(xiàn)私有屬性和私有方法。

  1. 極簡(jiǎn)主義法

這種方法用一個(gè)對(duì)象模擬類,在對(duì)象里定義一個(gè)函數(shù) createNew()模擬構(gòu)造函數(shù)的功能,來生成實(shí)例。

var Creature = {
  blood: 'red', //瞎寫的,有些生物的血不是紅色的:)
}
var People = {
  species: 'human',
  createNew: function(name) {
    var people = {}
    people.name = name
    return people
  },
}

這個(gè)方法可以既可以有私有屬性和方法,而且也能比較語義化的定義實(shí)例的各種屬性。但是這個(gè)方法有一個(gè)缺點(diǎn),那就是在繼承的時(shí)候沒有用到 prototype 的概念,也就是沒有用到 js 自己的原型鏈的概念。會(huì)讓人覺得不夠直觀。

總而言之,ES5 沒有類,哪怕是模擬實(shí)現(xiàn)了類,也不夠完美。

ES6 中的類

ES6 提供了更接近傳統(tǒng)語言的寫法,引入了 Class(類)這個(gè)概念,作為對(duì)象的模板。通過 class 關(guān)鍵字,可以定義類?;旧?,ES6 的 class 可以看作只是一個(gè)語法糖,它的絕大部分功能,ES5 都可以做到,新的 class 寫法只是讓對(duì)象原型的寫法更加清晰、更像面向?qū)ο缶幊痰恼Z法而已。

//定義類
let methodName = 'getArea'
class Foo {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')'
  }
  [methodName]() {
    return 'area_' + this.x + '_' + this.y
  }
}
let foo = foo('a', 'b') //報(bào)錯(cuò)
let foo = new Foo('a', 'b')
typeof Foo // 'function'
Foo === Foo.prototype.constructor // true
foo.__proto__ === Foo.prototype // true
foo.toString === Foo.prototype.toString //true

可以看到,類的所有方法都定義在類的 prototype 屬性上面。在類的實(shí)例上面調(diào)用方法,其實(shí)就是調(diào)用原型上的方法。類的屬性名,可以采用表達(dá)式。類的內(nèi)部所有定義的方法,都是不可枚舉的(non-enumerable)。這一點(diǎn)與 ES5 的行為不一致。類必須使用 new 調(diào)用,否則會(huì)報(bào)錯(cuò)。這是它跟普通構(gòu)造函數(shù)的一個(gè)主要區(qū)別,后者不用 new 也可以執(zhí)行。類不存在變量提升(hoist),這一點(diǎn)與 ES5 完全不同。

new.targer 屬性

new 是從構(gòu)造函數(shù)生成實(shí)例對(duì)象的命令。ES6 為 new 命令引入了一個(gè) new.target 屬性,該屬性一般用在構(gòu)造函數(shù)之中,返回 new 命令作用于的那個(gè)構(gòu)造函數(shù)。如果構(gòu)造函數(shù)不是通過 new 命令調(diào)用的,new.target 會(huì)返回 undefined,因此這個(gè)屬性可以用來確定構(gòu)造函數(shù)是怎么調(diào)用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name
  } else {
    throw new Error('必須使用 new 命令生成實(shí)例')
  }
}

// 另一種寫法
function Person(name) {
  if (new.target === Person) {
    this.name = name
  } else {
    throw new Error('必須使用 new 命令生成實(shí)例')
  }
}

var person = new Person('張三') // 正確
var notAPerson = Person.call(person, '張三') // 報(bào)錯(cuò)

javascript 類的繼承

ES5 中類的繼承

  1. 構(gòu)造函數(shù)綁定

使用 call 或 apply 方法,將父對(duì)象的構(gòu)造函數(shù)綁定在子對(duì)象上,即在子對(duì)象構(gòu)造函數(shù)中加一行:

function Animal() {
  this.species = 'animal'
}
function Cat(name, color) {
  Animal.apply(this, arguments)
  this.name = name
  this.color = color
}

var cat1 = new Cat('miao', 'black')

alert(cat1.species) // 動(dòng)物

這樣能夠達(dá)到繼承的目的,但是問題是,它會(huì)為所有的 Cat 實(shí)例都創(chuàng)建一個(gè)自己的 species 屬性,這樣會(huì)浪費(fèi)內(nèi)存,顯然是不太優(yōu)雅的做法

  1. 繼承 prototype

由于 Animal 對(duì)象中,不變的屬性都可以直接寫入 Animal.prototype。所以,我們也可以讓 Cat()跳過 Animal(),直接繼承 Animal.prototype。

function Animal() {}
Animal.prototype.species = 'animal'
Cat.prototype = Animal.prototype
// 因?yàn)镃at的prototype直接指向了Animal的prototype,因此它的constructor屬性也發(fā)生了變化,需要手動(dòng)糾正Cat的構(gòu)造函數(shù)
console.log(Cat.prototype.constructor === Animal) // true
Cat.prototype.constructor = Cat
console.log(Animal.prototype.constructor === Cat) // true
var cat1 = new Cat('miao', 'black')

alert(cat1.species) // animal

這里出現(xiàn)了一個(gè)問題,由于是直接賦值的,所以 Cat 的 prototype 和 Animal 的 prototype 指向了同一個(gè)對(duì)象,在修改 Cat 的 prototype.constructor 的時(shí)候,Animal.prototype.constructor 也隨之被更改。因此可以采用一個(gè)中間量過渡。

var F = function() {}
F.prototype = Animal.prototype
Cat.prototype = new F()
Cat.prototype.constructor = Cat
console.log(Cat.prototype === Animal.prototype) // false

這樣,就能解決上述問題了。

ES6 中類的繼承

Class 可以通過extends關(guān)鍵字實(shí)現(xiàn)繼承,這比 ES5 的通過修改原型鏈實(shí)現(xiàn)繼承,要清晰和方便很多。

class Foo {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  static hello(){
    console.log('hello world')
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')'
  }
}
class ChildFoo extends Foo {
  constructor(x, y, z) {
    this.z=z //報(bào)錯(cuò)
    super(x, y); // 調(diào)用父類的constructor(x, y)
    this.z = z;
  }

  toString() {
    return this.z + ' ' + super.toString(); // 調(diào)用父類的toString()
  }
}
console.log(ChildFoo.hello===ChildFoo.__proto__.hello) // true
console.log(ChildFoo.hello===Foo.hello) // true

子類必須在constructor方法中調(diào)用super方法,否則新建實(shí)例時(shí)會(huì)報(bào)錯(cuò)。這是因?yàn)樽宇悰]有自己的this對(duì)象,而是繼承父類的this對(duì)象,然后對(duì)其進(jìn)行加工。如果不調(diào)用super方法,子類就得不到this對(duì)象。此時(shí)子類調(diào)用this對(duì)象,就會(huì)報(bào)錯(cuò)。
ES5 的繼承,實(shí)質(zhì)是先創(chuàng)造子類的實(shí)例對(duì)象this,然后再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機(jī)制完全不同,實(shí)質(zhì)是先創(chuàng)造父類的實(shí)例對(duì)象this(所以必須先調(diào)用super方法),然后再用子類的構(gòu)造函數(shù)修改this。
最后,父類的靜態(tài)方法,也會(huì)被子類繼承。

javascript 原型和原型鏈,以及prototype和proto

什么是原型?
每個(gè)javascript對(duì)象都有一個(gè)私有屬性,稱之為Prototype(注意這里只是一個(gè)稱謂,非一個(gè)屬性),翻譯過來就是原型,它被現(xiàn)今大多數(shù)瀏覽器實(shí)現(xiàn)為proto屬性。這個(gè)proto屬性,指向?qū)ο蟮念悾?gòu)造函數(shù))的prototype屬性,這個(gè)屬性也被稱之為原型對(duì)象(注意和前面的原型不是一個(gè)概念)。每一個(gè)對(duì)象(null除外)在創(chuàng)建的時(shí)候就會(huì)從原型"繼承"它所有的屬性。
上述用代碼表示就是:

function Person(name) {
  this.name=name
}
let person = new Person();

// 實(shí)例化之后的對(duì)象,這個(gè)對(duì)象的原型指向它的類的原型對(duì)象
console.log(person.__proto__===Person.prototype) // true

什么是原型鏈?
JavaScript 對(duì)象有一個(gè)指向一個(gè)原型對(duì)象的鏈。當(dāng)試圖訪問一個(gè)對(duì)象的屬性時(shí),它不僅僅在該對(duì)象上搜尋,還會(huì)搜尋該對(duì)象的原型,以及該對(duì)象的原型的原型,依次層層向上搜索,直到找到一個(gè)名字匹配的屬性或到達(dá)原型鏈的末尾。
上述用代碼表示就是:

function Creature(){}
Creature.prototype.blood='red'
function Person(name) {
  this.name=name
}

// 將Person作為Creature的子類
Person.prototype=new Creature()

let person = new Person();

// 注意,下面是兩段偽代碼,實(shí)際運(yùn)行后不會(huì)得到true,原因很簡(jiǎn)單,本文就不討論了,請(qǐng)自行思考 :)

// 沿著等號(hào)往后走,這就是一條原型鏈。
console.log(person.blood===person.__proto__.blood=== Person.prototype.blood===Person.prototype.__proto__.blood===Creature.prototype.blood) // true

// 這是一個(gè)在原型鏈上沒有找到指定屬性的例子,原型鏈會(huì)一直延伸到最末端
console.log(person.tail===person.__proto__.tail=== Person.prototype.tail===Person.prototype.__proto__.tail===Creature.prototype.tail===Creature.prototype.__proto__.tail===Object.prototype.tail===undefined)

代碼中可以看到,當(dāng)訪問person對(duì)象的blood屬性的時(shí)候,顯然本地是沒有這個(gè)屬性的,那么就會(huì)去尋找它的原型(即proto,也即Person.prototype)是否有這個(gè)屬性,結(jié)果它的原型也沒有這個(gè)屬性。它的原型Person.prototype本身同樣也是一個(gè)對(duì)象,那么它也有自己的原型,此時(shí)它會(huì)在自己的原型上繼續(xù)尋找這個(gè)屬性,也就是Person.prototype.__proto__,而Person.prototype.__proto__指向的是Creature.prototype,在Creature.prototype上找到了blood這個(gè)屬性,原型鏈結(jié)束。

有關(guān) prototype 的方法

使用proto是有爭(zhēng)議的,官方也不鼓勵(lì)使用它。因?yàn)樗鼜膩頉]有被包括在EcmaScript語言規(guī)范中,但是現(xiàn)代瀏覽器都實(shí)現(xiàn)了它。
不過proto屬性已在ECMAScript 6語言規(guī)范中標(biāo)準(zhǔn)化,用于確保Web瀏覽器的兼容性,因此它未來將被支持。
但是官方依然不推薦使用它,而是推出了一系列的有關(guān)原型的方法。

Object.getPrototypeOf(object)

此方法放回給定對(duì)象的原型,注意這里的原型是指proto,而非prototype屬性。

var proto = {};
var obj = Object.create(proto);
Object.getPrototypeOf(obj) === proto; // true

Object.setPrototypeOf(obj,prototype)

此方法可以重新設(shè)置一個(gè)對(duì)象的原型,同樣,這里的原型依然指的是proto,其實(shí)就基本等同于Object.create()方法。

var proto={}
var obj={}
Object.setPrototypeOf(obj,proto)
obj.__proto__===proto

prototypeObj.prototype.isPrototypeOf(object)

此方法用來檢查一個(gè)對(duì)象是否是另一個(gè)對(duì)象的原型,可以理解為等同于

prototypeObj.prototype===object.__proto__

私有方法/屬性、靜態(tài)方法/屬性、公共方法/屬性、實(shí)例方法/屬性

經(jīng)常會(huì)聽到體積一些私有屬性公有屬性,靜態(tài)屬性靜態(tài)方法之類的詞匯,趁此機(jī)會(huì),也對(duì)這些概念做一個(gè)系統(tǒng)的梳理。

私有方法/屬性

指在類的內(nèi)部運(yùn)算使用的方法和屬性,而其他人無論是通過這個(gè)類還是通過這個(gè)類的實(shí)例都無法訪問。
其實(shí)很好理解為什么ES6的類沒有私有屬性和私有方法,因?yàn)樗鼪]有目前提供這個(gè)語法。
給一個(gè)變量賦值,無非兩種方法,一種是等號(hào),一種是聲明式的,而ES6的類內(nèi)部的語法很有限,并不能通過任何方法來進(jìn)行一個(gè)變量賦值,等號(hào)的話不能識(shí)別,而直接聲明的函數(shù)又會(huì)被解析為公有方法。
所以那些模擬的私有方法和屬性也就容易理解了,其實(shí)就是要達(dá)到在類的內(nèi)部能夠全局使用,而脫離了這個(gè)類就無法訪問的目的。

靜態(tài)方法/屬性

是指這個(gè)類自己的屬性和方法,不會(huì)被繼承,而是通過類本身來調(diào)用。比如Creature這個(gè)類,那么他的靜態(tài)方法可以通過Creature.someProperty這樣的方式訪問到

公共方法/屬性

其實(shí)就可以簡(jiǎn)單的理解為原型對(duì)象(prototype)上的方法和屬性,所有的實(shí)例化的對(duì)象都是能繼承到的和共享的。

實(shí)例方法/屬性

就是constructor函數(shù)里this后面的一些賦值的方法和屬性,是沒有實(shí)例自己私有的。

參考文檔:

http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance_continued.html
http://www.ruanyifeng.com/blog/2012/07/three_ways_to_define_a_javascript_class.html
http://es6.ruanyifeng.com/#docs/class
http://es6.ruanyifeng.com/#docs/class-extends
https://developer.mozilla.org/zh-cn/docs/web/javascript/reference/global_objects/object
https://stackoverflow.com/questions/41189190/how-and-why-would-i-write-a-class-that-extends-null
https://github.com/mqyqingfeng/Blog/issues/2
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容