App下載

JavaScript原型與繼承的秘密

猿友 2021-02-23 15:58:13 瀏覽數(shù) (6832)
反饋

JavaScript 的原型與繼承是每一個(gè)學(xué)習(xí) JavaScript 的同學(xué)都會(huì)面對(duì)的一個(gè)問(wèn)題,也是很多面試的必考題目; 但是經(jīng)常會(huì)有一些同學(xué)對(duì)此一知半解,或者是淺嘗輒止;這是因?yàn)楹芏嘀v解原型與繼承的文章寫的不是那么通俗易懂, 而本文的目的就是一次性的幫助大家把這一系列的知識(shí)點(diǎn)梳理清楚;希望我這次能夠做一個(gè)好的投球手。

首先我們需要知道的是,JavaScript 是一種動(dòng)態(tài)語(yǔ)言,本質(zhì)上說(shuō)它是沒(méi)有?Class?(類)的;但是它也需要一種繼承的方式, 那就是原型繼承;JavaScript 對(duì)象的一些屬性和方法都是繼承自別的對(duì)象。

很多同學(xué)對(duì) JavaScript 的原型和繼承不是很理解,一個(gè)重要的原因就是大家沒(méi)有理解?__proto__?和?prototype?這兩個(gè)屬性的意思。 接下來(lái)我們先來(lái)好好梳理一下這兩個(gè)屬性,看看它們存在哪里,代表了什么意義,又有什么作用。

首先來(lái)說(shuō)一下?__proto__?這個(gè)屬性吧,我們需要知道的是,除了?null?和?undefined?,?JavaScript?中的所有數(shù)據(jù)類型都有這個(gè)屬性; 它表示的意義是:當(dāng)我們?cè)L問(wèn)一個(gè)對(duì)象的某個(gè)屬性的時(shí)候,如果這個(gè)對(duì)象自身不存在這個(gè)屬性, 那么就從這個(gè)對(duì)象的?__proto__?(為了方便下面描述,這里暫且把這個(gè)屬性稱作?p0?)屬性上面 繼續(xù)查找這個(gè)屬性,如果p0上面還存在?__proto__?(p1)屬性的話,那么就會(huì)繼續(xù)在p1上面查找響應(yīng)的屬性, 直到查找到這個(gè)屬性,或者沒(méi)有?__proto__?屬性為止。 我們可以用下面這兩幅圖來(lái)表示:

微信截圖_20210223094204

上面這幅圖表示在obj原型鏈上面找到了屬性名字是a的值

微信截圖_20210223094256

上面這幅圖表示在obj的原型鏈上面沒(méi)有找到屬性名字是 a 的值

我們把一個(gè)對(duì)象的?__proto__?屬性所指向的對(duì)象,叫做這個(gè)對(duì)象的原型;我們可以修改一個(gè)對(duì)象的原型來(lái)讓這個(gè)對(duì)象擁有某種屬性,或者某個(gè)方法。

// 修改一個(gè)Number類型的值的原型
const num = 1;
num.__proto__.name = "My name is 1";
console.log(num.name); // My name is 1

// 修改一個(gè)對(duì)象的原型
const obj = {};
obj.__proto__.name = "dreamapple";
console.log(obj.name); // dreamapple

這里需要特別注意的是,?__proto__?這個(gè)屬性雖然被大多數(shù)的瀏覽器支持,但是其實(shí)它僅在?ECMAScript 2015 規(guī)范?中被準(zhǔn)確的定義, 目的是為了給這個(gè)傳統(tǒng)的功能定制一個(gè)標(biāo)準(zhǔn),以確保瀏覽器之間的兼容性。通過(guò)使用?__proto__?屬性來(lái)修改一個(gè)對(duì)象的原型是非常慢且影響性能的一種操作。 所以,現(xiàn)在如果我們想要獲取一個(gè)對(duì)象的原型,推薦使用?Object.getPrototypeOf ?或者?Reflect.getPrototypeOf?,設(shè)置一個(gè)對(duì)象的原型推薦使用?Object.setPrototypeOf?或者是?Reflect.setPrototypeOf?。

到這里為止,我們來(lái)對(duì)?__proto__?屬性做一個(gè)總結(jié):

  • 存在哪里? 除了?null?和?undefined?所有其他的JavaScript對(duì)象或者原始類型都有這個(gè)屬性
  • 代表了什么? 表示了一個(gè)對(duì)象的原型
  • 有什么作用? 可以獲取和修改一個(gè)對(duì)象的原型

說(shuō)完?__proto__?屬性,接下來(lái)我們就要好好的來(lái)理解一下?prototype?屬性了;首先我們需要記住的是,這個(gè)屬性一般只存在于函數(shù)對(duì)象上面; 只要是能夠作為構(gòu)造器的函數(shù),他們都包含這個(gè)屬性。也就是說(shuō),只要這個(gè)函數(shù)能夠通過(guò)使用?new?操作符來(lái)生成一個(gè)新的對(duì)象, 那么這個(gè)函數(shù)肯定具有?prototype?屬性。因?yàn)槲覀冏远x的函數(shù)都可以通過(guò)?new?操作符生成一個(gè)對(duì)象,所以我們自定義的函數(shù)都有?prototype? 這個(gè)屬性。

// 函數(shù)字面量
console.log((function(){}).prototype); // {constructor: ?}

// Date構(gòu)造器
console.log(Date.prototype); // {constructor: ?, toString: ?, toDateString: ?, toTimeString: ?, toISOString: ?, …}

// Math.abs 不是構(gòu)造器,不能通過(guò)new操作符生成一個(gè)新的對(duì)象,所以不含有prototype屬性
console.log(Math.abs.prototype); // undefined

那這個(gè)prototype屬性有什么作用呢?這個(gè)prototype屬性的作用就是:函數(shù)通過(guò)使用new操作符生成的一個(gè)對(duì)象, 這個(gè)對(duì)象的原型(也就是__proto__)指向該函數(shù)的prototype屬性。 那么一個(gè)比較簡(jiǎn)潔的表示__proto__prototype 屬性之間關(guān)系的等式也就出來(lái)了,如下所示:

// 其中F表示一個(gè)自定義的函數(shù)或者是含有prototype屬性的內(nèi)置函數(shù)
new F().__proto__ === F.prototype // true

我們可以使用下面這張圖來(lái)更加形象的表示上面這種關(guān)系:

微信截圖_20210223094855

看到上面等式,我想大家對(duì)于?__proto__?和?prototype?之間關(guān)系的理解應(yīng)該會(huì)更深一層了。

好,接下來(lái)我們對(duì)?prototype?屬性也做一個(gè)總結(jié):

  • 存在哪里? 自定義的函數(shù),或者能夠通過(guò)?new?操作符生成一個(gè)對(duì)象的內(nèi)置函數(shù)
  • 代表了什么? 它表示了某個(gè)函數(shù)通過(guò)?new?操作符生成的對(duì)象的原型
  • 有什么作用? 可以讓一個(gè)函數(shù)通過(guò)?new?操作符生成的許多對(duì)象共享一些方法和屬性

其實(shí)到這里為止,關(guān)于JavaScript的原型和繼承已經(jīng)講得差不多了;下面的內(nèi)容是一些基于上面的一些拓展, 可以讓你更好地理解我們上面所說(shuō)的。

當(dāng)我們理解了上面的知識(shí)點(diǎn)之后,我們就可以對(duì)下面的表達(dá)式做一個(gè)判斷了:

// 因?yàn)镺bject是一個(gè)函數(shù),函數(shù)的構(gòu)造器都是Function
Object.__proto__ === Function.prototype // true

// 通過(guò)函數(shù)字面量定義的函數(shù)的__proto__屬性都指向Function.prototype
(function(){}).__proto__ === Function.prototype // true

// 通過(guò)對(duì)象字面量定義的對(duì)象的__proto__屬性都是指向Object.prototype
({}).__proto__ === Object.prototype // true

// Object函數(shù)的原型的__proto__屬性指向null
Object.prototype.__proto__ === null // true

// 因?yàn)镕unction本身也是一個(gè)函數(shù),所以Function函數(shù)的__proto__屬性指向它自身的prototype
Function.__proto__ === Function.prototype // true

// 因?yàn)镕unction的prototype是一個(gè)對(duì)象,所以Function.prototype的__proto__屬性指向Object.prototype
Function.prototype.__proto__ === Object.prototype // true

如果你能夠把上面的表達(dá)式都梳理清楚的話,那么說(shuō)明你對(duì)這部分知識(shí)掌握的還是不錯(cuò)的。

談及JavaScript的原型和繼承,那么我們還需要知道另一個(gè)概念;那就是?constructor?,那什么是?constructor?呢? ?constructor?表示一個(gè)對(duì)象的構(gòu)造函數(shù),除了?null?和?undefined?以外,JavaScript中的所有數(shù)據(jù)類型都有這個(gè)屬性; 我們可以通過(guò)下面的代碼來(lái)驗(yàn)證一下:

null.constructor // Uncaught TypeError: Cannot read property 'constructor' of null ...
undefined.constructor // Uncaught TypeError: Cannot read property 'constructor' of undefined ...

(true).constructor // ? Boolean() { [native code] }
(1).constructor // ? Number() { [native code] }
"hello".constructor // ? String() { [native code] }

我們還可以使用下面的圖來(lái)更加具體的表現(xiàn):

微信截圖_20210223095236

但是其實(shí)上面這張圖的表示并不算準(zhǔn)確,因?yàn)橐粋€(gè)對(duì)象的constructor屬性確切地說(shuō)并不是存在這個(gè)對(duì)象上面的; 而是存在這個(gè)對(duì)象的原型上面的(如果是多級(jí)繼承需要手動(dòng)修改原型的constructor屬性,見(jiàn)文章末尾的代碼),我們可以使用下面的代碼來(lái)解釋一下:

const F = function() {};
// 當(dāng)我們定義一個(gè)函數(shù)的時(shí)候,這個(gè)函數(shù)的prototype屬性上面的constructor屬性指向自己本身
F.prototype.constructor === F; // true

下面的圖片形象的展示了上面的代碼所表示的內(nèi)容:

微信截圖_20210223095342

關(guān)于constructor還有一些需要注意的問(wèn)題,對(duì)與JavaScript的原始類型來(lái)說(shuō),它們的constructor屬性是只讀的,不可以修改。 我們可以通過(guò)下面的代碼來(lái)驗(yàn)證一下:

(1).constructor = "something";
console.log((1).constructor); // 輸出 ? Number() { [native code] }

當(dāng)然,如果你真的想更改這些原始類型的constructor屬性的話,也不是不可以,你可以通過(guò)下面的方式來(lái)進(jìn)行修改:

Number.prototype.constructor = "number constructor";
(1).constructor = 1;
console.log((1).constructor); // 輸出 number constructor

當(dāng)然上面的方式我們是不推薦你在真實(shí)的開發(fā)中去使用的,接下來(lái),我會(huì)使用一些代碼來(lái)把今天講解的知識(shí)再大致的回顧一下:

function Animal(name) {
  this.name = name;
}

Animal.prototype.setName = function(name) {
  this.name = name;
};
Animal.prototype.getName = function(name) {
  return this.name;
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);

// 因?yàn)樯厦娴恼Z(yǔ)句將我們?cè)瓉?lái)的prototype的指向修改了,所以我們要重新定義Dog的prototype屬性的constructor屬性
Reflect.defineProperty(Dog.prototype, "constructor", {
  value: Dog,
  enumerable: false, // 不可枚舉
  writable: true
});

const animal = new Animal("potato");
console.log(animal.__proto__ === Animal.prototype); // true
console.log(animal.constructor === Animal); // true
console.log(animal.name); // potato

const dog = new Dog("potato", "labrador");
console.log(dog.name); // potato
console.log(dog.breed); // labrador
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.constructor === Dog); // true

推薦好課:JavaScript微課,ES6微課


0 人點(diǎn)贊