摘要:問題修改實例的,即修改了構造函數的原型對象的共享屬性到此處,涉及到的內容大家可以再回頭捋一遍,理解了就會覺得醍醐灌頂。
開場白
大三下學期結束時候,一個人跑到帝都來參加各廠的面試,免不了的面試過程中經常被問到的問題就是JS中如何實現繼承,當時的自己也是背熟了實現繼承的各種方法,回過頭來想想卻不知道__proto__是什么,prototype是什么,以及各種繼承方法的優(yōu)點和缺點,想必有好多剛入坑的小伙伴有著跟我一樣的體驗,這篇文章將從基礎概念出發(fā),進一步說明js繼承,以及各種繼承方法的優(yōu)缺點,希望對看這篇文章的你有所幫助,如果你是見多識廣的大佬,既然看到這里了,不妨繼續(xù)看下去,指點一二,讓新入坑的小伙伴更好的成長。(如果你都看到這了,透露一下文末有彩蛋嗷!)下面,我們進入正題:
設計思想如果你沒看過,也會聽別人說JavaScript的繼承不同于Java和c++,js中沒有“類”和“實例”的區(qū)分,而是靠一種原型鏈的一級一級的指向來實現繼承。那么當時的創(chuàng)造JavaScript這種的語言的人為什么要這樣實現js獨有的繼承,大家可以閱讀阮一峰老師的Javascript繼承機制的設計思想,就像講故事一樣,從古代至現代說明了js繼承這種設計模式的緣由。
prototype對象了解了js繼承的設計思想后,我們需要學習原型鏈上的第一個屬性prototype,這個屬性是一個指針,指向的是原型對象的內存堆。從阮一峰老師的文章中,我們可以知道prototype是為了解決構造函數的屬性和方法不能共享的問題而提出的,下面我們先實現一個簡單的繼承:
function constructorFn (state, data) { this.data = data; this.state = state; this.isPlay = function () { return this.state + " is " + this.data; } } var instance1 = new constructorFn ("1", "doing"); var instance2 = new constructorFn ("0", "done"); console.log(instance1.isPlay()); // 1 is doing console.log(instance2.isPlay()); // 0 is done
此時,實例1 和實例2 都有自己的data屬性、state屬性、isPlay方法,造成了資源的浪費,既然兩個實例都需要調用isPlay方法,便可以將isPlay方法掛載到構造函數的prototype對象上,實例便有了本地屬性方法和引用屬性方法,如下:
function constructorFn (state, data) { this.data = data; this.state = state; } constructorFn.prototype.isPlay = function () { return this.state + " is " + this.data; } constructorFn.prototype.isDoing = "nonono!"; var instance1 = new constructorFn ("1", "doing"); var instance2 = new constructorFn ("0", "done"); console.log(instance1.isPlay()); // 1 is doing console.log(instance2.isPlay()); // 0 is done console.log(instance1.isDoing); // nonono! console.log(instance2.isDoing); // nonono!
我們將isPlay方法掛載到prototype對象上,同時增加isDoing屬性,既然是共享的屬性和方法,那么修改prototype對象的屬性和方法,實例的值都會被修改,如下:
constructorFn.prototype.isDoing = "yesyesyes!"; console.log(instance1.isDoing); // yesyesyes! console.log(instance2.isDoing); // yesyesyes!
問題來了,為什么實例會取到prototype對象上的屬性和方法,別急,沒多久就會結合其他問題綜合解答。
同時,你可能會問,如果修改實例1的isDoing屬性的原型,實例2的isDoing會不會受到影響?
instance1.isDoing = "yesyesyes!"; console.log(instance1.isDoing); // yesyesyes! console.log(instance2.isDoing); // nonono!
問題又來了,可以看到修改實例1的isDoing屬性,實例2的實例并未受到影響。這是為什么呢?
那如果修改實例1的isDoing屬性的原型屬性,實例2的isDoing會不會受到影響?如下:
instance1.__proto__.isDoing = "yesyesyes!"; console.log(instance1.isDoing); // yesyesyes! console.log(instance2.isDoing); // yesyesyes!
問題又又來了,為什么修改實例1的__proto__屬性上的isDoing的值就會影響到構造函數的原型對象的屬性值?
我們先整理一下,未解決的三個問題:
為什么實例會取到prototype對象上的屬性和方法?
為什么修改實例1的isDoing屬性,實例2的實例沒有受到影響?
為什么修改實例1的__proto__屬性上的isDoing的值就會影響到構造函數的原型對象的屬性值?
這時候不得不背后真正的操作者搬出來了,就是new操作符,同樣是面試最火爆的問題之一,new操作符干了什么?相信有人也是跟我一樣,已經背的滾瓜爛熟了,以 Var instance1 = new constructorFn();為例,就是下面三行代碼:
var obj = {}; obj.__proto__ = constructorFn.prototype; constructorFn.call(obj);
第一行聲明一個空對象,因為實例本身就是一個對象。
第二行將實例本身的__proto__屬性指向構造函數的原型,obj新增了構造函數prototype對象上掛載的屬性和方法。
第三行將構造函數的this指向替換成obj,再執(zhí)行構造函數,obj新增了構造函數本地的屬性和方法。
理解了上面三行代碼的含義,那么三個問題也就迎刃而解了。
問題1:實例在新建的時候,本身的__ptoto__指向了構造函數的原型。
問題2:實例1和實例2 在新建后,有了各自的this,修改實例1的isDoing屬性,只是修改了當前對象的isDoing的屬性值,并沒有影響到構造函數。
問題3:修改實例1的__proto__,即修改了構造函數的原型對象的共享屬性
到此處,涉及到的內容大家可以再回頭捋一遍,理解了就會覺得醍醐灌頂。
__proto__同時,你可能又會問,__proto__是什么?
簡單來說,__proto__是對象的一個隱性屬性,同時也是一個指針,可以設置實例的原型。
實例的__proto__指向構造函數的原型對象。
需要注意的是,
每個對象都有內置的__proto__屬性,函數對象才會有prototype屬性。用chrome和FF都可以訪問到對象的__proto__屬性,IE不可以。
我們繼續(xù)用上面的例子來說明:
function constructorFn (state, data) { this.data = data; this.state = state; } constructorFn.prototype.isPlay = function () { return this.state + " is " + this.data; } constructorFn.prototype.isDoing = "nonono!"; var instance1 = new constructorFn ("1", "doing"); console.log(instance1.__proto__ === constructorFn.prototype); // true
構造函數的原型對象也是對象,那么constructor.prototype.__proto__指向誰呢?
定義中說對象的__proto__指向的是構造函數的原型對象,下面我們驗證一下constructor.prototype.__proto__的指向:
console.log(instance1.__proto__ === constructorFn.prototype); // true console.log(constructorFn.prototype.__proto__ === Object.prototype) // true
用圖形表示的話,如下:
可以看出,constructor.prototype.__proto__的指向是Object的原型對象。
那么,Object.prototype.__proto__的指向呢?
console.log(instance1.__proto__ === constructorFn.prototype); // true console.log(constructorFn.prototype.__proto__ === Object.prototype) // true console.log(Object.prototype.__proto__); // null
用圖形表示的話,如下:
可以發(fā)現,Object.prototype.__proto__ === null;
這樣也就形成了原型鏈。通過將實例的原型指向構造函數的原型對象的方式,連通了實例-構造函數-構造函數的原型,原型鏈的特點就是逐層查找,從實例開始查找一層一層,找到就返回,沒有就繼續(xù)往上找,直到所有對象的原型Object.prototype。
了解了上面的基礎概念,就要將學到的用在實際當中,到底要怎么實現繼承呢?實現的方式有哪些?下面主要說明實現繼承最常用的三用方式,可以滿足基本的開發(fā)需求,想要更深入的了解,可以參考阮一峰老師的網絡博客。
原型鏈繼承
實現原理:將父類的實例作為子類的原型
function Animal (name) { this.name = name; } Animal.prototype = { canRun: function () { console.log("it can run!"); } } function Cat () { this.speak = "喵!"; } Cat.prototype = new Animal("miao"); Cat.prototype.constructor = Cat;
注:
這種繼承方式需要將子類的構造函數指回本身,因為從父類繼承時同時也繼承了父類的構造函數。
簡單的使用Cat.prototype = Animal.prototype將會導致兩個對象共享相同的原型,一個改變另一個也會改變。
不要使用Cat.prototype = Animal,因為不會執(zhí)行Animal的原型,而是指向函數Animal。因此原型鏈將會回溯到Function.prototype,而不是Animal.prototype,因此canRun將不會在Cat的原型鏈上。
使用call、apply方法實現
實現原理:改變函數的this指向
function Animal (name) { this.name = name; } Animal.prototype = { canRun: function () { console.log("it can run!"); } } function Cat (name) { Animal.call(this, name); this.speak = "喵!"; }
注:
該方法將子類Cat的this指向父類Animal,但是并沒有拿到父類原型對象上的屬性和方法
使用混合方法實現
實現原理:原型鏈可以繼承原型對象的屬性和方法,構造函數可以繼承實例的屬性且可以給父類傳參
function Animal (name) { this.name = name; } Animal.prototype = { canRun: function () { console.log("it can run!"); } } function Cat (name, age) { Animal.call(this, name); this.speak = "喵!"; this.age = age; } Cat.prototype = new Animal(); Cat.prototype.constructor = Cat; var cat = new Cat("tom", "12");
每一種繼承方式都有自己的優(yōu)點和不足,讀者可以根據實際情況選擇相應的方法。為了在實際開發(fā)中更方便的使用繼承,可以封裝一個繼承的方法,如下:
function extend (child, parent) { var F = function () {}; F.prototype = parent.prototype; child.prototype = new F(); child.prototype.construtor = child; child.superObj = parent.prototype; //修正原型的constructor指向 if(!parent.prototype.contrucotor == Object.prototype.constructor){ parent.prototype.constructor = parent; } }
結合一開始的例子,可以這樣實現繼承的關系:
function constructorFn (state, data) { this.data = data; this.state = state; } constructorFn.prototype.isPlay = function () { return this.state + " is " + this.data; } constructorFn.prototype.isDoing = "nonono!"; function subFn (state, data) { subFn.superObj.constructor.call(this, state, data); //從superFn.constructor中調用 } extend(subFn, constructorFn ); // 獲取構造函數原型上的屬性和方法
javaScript的繼承遠不止這些,,只希望可以讓新學js的小伙伴不那么盲目的去刻意記一些東西,當然學習最好的辦法還是要多寫,最簡單的就是直接打開瀏覽器的控制臺,去驗證自己各種奇奇怪怪的想法,動起來吧~
|-------赤裸裸的分割線-------|
彩蛋來啦:本周我們的客戶端app 5.6版本就要正式發(fā)版啦,新版本新增了小視頻功能呢,大家可以通過小視頻分享自己各種購物經驗,也可以發(fā)揮自己的腦洞,展示自己的才華,快來給我們的開發(fā)小哥哥打call吧~
快速入口
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98024.html
摘要:靜態(tài)屬性靜態(tài)方法目前支持靜態(tài)方法表示,類屬性及靜態(tài)屬性目前作為提案還未正式成為標準。在中,抽象類不能用來實例化對象,主要做為其它派生類的基類使用。不同于接口,抽象類可以包含成員的實現細節(jié)。中也是這樣規(guī)定的抽象類不允許直接被實例化。 嘗試重寫 在此之前,通過《JavaScript => TypeScript 入門》已經掌握了類型聲明的寫法。原以為憑著那一條無往不利的規(guī)則,就可以開開心心的...
摘要:函數式編程前端掘金引言面向對象編程一直以來都是中的主導范式。函數式編程是一種強調減少對程序外部狀態(tài)產生改變的方式。 JavaScript 函數式編程 - 前端 - 掘金引言 面向對象編程一直以來都是JavaScript中的主導范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數式編程越來越多得受到開發(fā)者的青睞。函數式編程是一種強調減少對程序外部狀態(tài)產生改變的方式。因此,...
摘要:本文是面向前端小白的,大手子可以跳過,寫的不好之處多多分鐘搞定常用基礎知識前端掘金基礎智商劃重點在實際開發(fā)中,已經非常普及了。 JavaScript字符串所有API全解密 - 掘金關于 我的博客:louis blog SF專欄:路易斯前端深度課 原文鏈接:JavaScript字符串所有API全解密 本文近 6k 字,讀完需 10 分鐘。 字符串作為基本的信息交流的橋梁,幾乎被所有的編程...
摘要:上下文切換上下文最直觀的表現就是代碼塊中的,通常在面向對象的編程中用到,來指代當前類生成的對應實例,與其他語言的一致。咦,是干嘛的,有沒有其他方式實現,請自行谷歌。 分享第一篇,關于 NodeJS —— Javascript 的常用知識以及如何從 Javascript 開發(fā)者過渡到 NodeJS 開發(fā)者(不會介紹具體的框架)。在讀本文前,希望你對 javascript 有一些初步的認識...
摘要:從入門到放棄是什么,黑歷史,不講,自己百度去。類你沒有看錯,這里面的就沒有問題的。之前我們用過,和有了,再也不用這兩個貨了。一個函數,可以遍歷狀態(tài)感覺就是狀態(tài)機,好吧不說了再說就懵逼了。 ES6從入門到放棄 1.ES6是什么,黑歷史,不講,自己百度去。 2.在瀏覽器中如何使用? 1.babel babeljs.io在線編譯 2.traceur-----Google出的編譯器,把E...
閱讀 3735·2023-01-11 11:02
閱讀 4244·2023-01-11 11:02
閱讀 3050·2023-01-11 11:02
閱讀 5180·2023-01-11 11:02
閱讀 4737·2023-01-11 11:02
閱讀 5534·2023-01-11 11:02
閱讀 5313·2023-01-11 11:02
閱讀 3990·2023-01-11 11:02