搜尋
首頁web前端js教程帶你徹底弄清楚Javascript繼承的原理!

這篇文章帶給大家的內容是關於帶你徹底弄清楚Javascript繼承的原理!有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

在理解繼承之前,需要知道js 的三個東西:

  1. 什麼是JS 原型鏈

  2. ##this 的值到底是什麼

  3. JS 的new 到底是做什麼的

一、什麼是JS 原型鏈?

我們知道JS 有對象,例如

    var obj = { name: 'obj' }
我們透過控制台把obj 印出來:

帶你徹底弄清楚Javascript繼承的原理!

我們會發現obj已經有幾個屬性(方法)了。

那麼問題來了:valueOf / toString / constructor 是怎麼來?我們並沒有給 obj.valueOf 賦值呀。

上面這張圖有點難懂,我手畫一個示意圖:

帶你徹底弄清楚Javascript繼承的原理!

#我們發現控制台打出來的結果是:

  1. obj本身有一個屬性name (這是我們給它加的)

  2. obj還有一個屬性叫做__proto__ (它是個物件)

  3. obj還有一個屬性,包括valueOf, toString, constructor等

  4. obj.__proto__其實也有一個叫做__proto__的屬性(console.log沒有顯示),值為null

現在回到我們的問題:obj 為什麼會擁有valueOf / toString / constructor 這幾個屬性?

答案: 這跟 __proto__有關 。

當我們「讀取」 obj.toString 時,JS 引擎會做下面的事情:

  1. 看看 obj 物件本身有沒有 toString 屬性。沒有就走到下一步。

  2. 看看obj.__proto__ 物件有沒有toString 屬性, 發現obj.__proto__ 有toString 屬性, 於是找到了,所以obj.toString其實就是第2步驟找到的obj.__proto__ .toString。

  3. 如果obj.__proto__沒有,那麼瀏覽器會繼續查看obj.__proto__.__proto__

  4. 如果obj.__proto__.__proto__也沒有,那麼瀏覽器會繼續查看obj.__proto__.__proto__.__proto__

#5.直到找到toString 或

proto 為null。

上面的過程,就是「讀」屬性的「搜尋過程」。而這個「搜尋過程」,是連著由

proto 組成的鍊子一直走的。 這個鍊子,就叫做「原型鏈」。

共享原型鏈

現在我們還有另一個物件

var obj2 = { name: 'obj2' }

#那麼obj.toString 和obj2.toString 其實是同一件東西, 也就是obj2.__proto__.toString。

說白了,我們改其中的一個__proto__.toString ,那麼另外一個其實也會改變!帶你徹底弄清楚Javascript繼承的原理!差異化

如果我們想讓obj.toString 和obj2.toString 的行為不同怎麼做呢?

直接賦值就好了:

obj.toString = function(){ return '新的 toString 方法' }


#小結帶你徹底弄清楚Javascript繼承的原理!

  1. ## [讀]屬性時會沿著原型鏈搜尋

[新增]屬性時不會去看原型鏈

二、this的值到底是什麼

你可能遇過這樣的JS 面試題:

var obj = {
  foo: function(){
    console.log(this)
  }
}

var bar = obj.foo
obj.foo() // 打印出的 this 是 obj
bar() // 打印出的 this 是 window

請解釋最後兩行函數的值為什麼不一樣。

函數呼叫

JS(ES5)裡面有三種函數呼叫形式:

func(p1, p2) 
obj.child.method(p1, p2)
func.call(context, p1, p2) // 先不讲 apply
一般,初學者都知道前兩種形式,而且認為前兩種形式“優於”第三種形式。

我們方方老師大姥說了,你一定要記住,第三種調用形式,才是正常調用形式:

    func.call(context, p1, p2)

其他兩種都是語法糖,可以等價地變為call 形式:

func(p1, p2)等價於func.call(undefined, p1, p2);

obj.child.method(p1, p2) 等價於obj .child.method.call(obj.child, p1, p2);至此我們的函數呼叫只有一種形式:

func.call(context, p1, p2)

這樣,this 就好解釋了

 this就是上面context。

this 是你 call 一個函數時傳的 context,由於你從來不用 call 形式的函數調用,所以你一直不知道。

###先看 func(p1, p2) 中的 this 如何確定:###
当你写下面代码时

function func(){
  console.log(this)
}

func()
等价于

function func(){
  console.log(this)
}

func.call(undefined) // 可以简写为 func.call()
###按理說打印出來的 this 應該就是 undefined 了吧,但是瀏覽器裡有一條規則:###如果你传的 context 就 null 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)

因此上面的打印结果是 window。如果你希望这里的 this 不是 window,很简单:

func.call(obj) // 那么里面的 this 就是 obj 对象了

回到题目:

var obj = {
  foo: function(){
    console.log(this)
  }
}

var bar = obj.foo
obj.foo() // 转换为 obj.foo.call(obj),this 就是 obj
bar() 
// 转换为 bar.call()
// 由于没有传 context
// 所以 this 就是 undefined
// 最后浏览器给你一个默认的 this —— window 对象

[ ] 语法

function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 这里面的 this 又是什么呢?

我们可以把 arr0 想象为arr.0( ),虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1, p2) 对应上了,于是就可以愉快的转换了:

    arr[0]()

假想为    arr.0()
然后转换为 arr.0.call(arr)
那么里面的 this 就是 arr 了 :)

小结:

  1. this 就是你 call 一个函数时,传入的第一个参数。

  2. 如果你的函数调用不是 call 形式, 请将其转换为 call 形式

三、JS 的 new 到底是干什么的?

我们声明一个士兵,具有如下属性:

var 士兵 = {
  ID: 1, // 用于区分每个士兵
  兵种:"美国大兵",
  攻击力:5,
  生命值:42, 
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

我们制造一个士兵, 只需要这样:

兵营.制造(士兵)

如果需要制造 100 个士兵怎么办呢?

循环 100 次吧:

var 士兵们 = []
var 士兵
for(var i=0; i<p>哎呀,看起来好简单</p><h4 id="质疑">质疑</h4><p>上面的代码存在一个问题:浪费了很多内存</p><ol class=" list-paddingleft-2">
<li><p>行走、奔跑、死亡、攻击、防御这五个动作对于每个士兵其实是一样的,只需要各自引用同一个函数就可以了,没必要重复创建 100 个行走、100个奔跑……</p></li>
<li><p>这些士兵的兵种和攻击力都是一样的,没必要创建 100 次。</p></li>
<li><p>只有 ID 和生命值需要创建 100 次,因为每个士兵有自己的 ID 和生命值。</p></li>
</ol><h4 id="改进">改进</h4><p>通过第一节可以知道 ,我们可以通过原型链来解决重复创建的问题:我们先创建一个「士兵原型」,然后让「士兵」的 <strong>proto</strong> 指向「士兵原型」。</p><pre class="brush:php;toolbar:false">var 士兵原型 = {
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

var 士兵们 = []
var 士兵
for(var i=0; i<h4 id="优雅">优雅?</h4><p>有人指出创建一个士兵的代码分散在两个地方很不优雅,于是我们用一个函数把这两部分联系起来:</p><pre class="brush:php;toolbar:false">function 士兵(ID){
  var 临时对象 = {};
  临时对象.__proto__ = 士兵.原型;
  临时对象.ID = ID;
  临时对象.生命值 = 42;
  
  return 临时对象;
}  

士兵.原型 = {
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

// 保存为文件:士兵.js

 然后就可以愉快地引用「士兵」来创建士兵了:

var 士兵们 = []
for(var i=0; i<p>JS 之父看到大家都这么搞,觉得何必呢,我给你们个糖吃,于是 JS 之父创建了 new 关键字,可以让我们少写几行代码:</p><p style="text-align: center;"><span class="img-wrap"><img src="/static/imghwm/default1.png" data-src="https://img.php.cn//upload/image/223/671/797/1538126235427481.png?x-oss-process=image/resize,p_40" class="lazy" title="1538126235427481.png" alt="帶你徹底弄清楚Javascript繼承的原理!"></span></p><p><strong>只要你在士兵前面使用 new 关键字,那么可以少做四件事情:</strong></p><ol class=" list-paddingleft-2">
<li><p>不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);</p></li>
<li><p>不用绑定原型,因为new 会帮你做(new 为了知道原型在哪,所以指定原型的名字 prototype);</p></li>
<li><p>不用 return 临时对象,因为 new 会帮你做;</p></li>
<li><p>不要给原型想名字了,因为 new 指定名字为 prototype。</p></li>
</ol><h4 id="这一次用-new-来写">这一次用 new 来写</h4><pre class="brush:php;toolbar:false">function 士兵(ID){
  this.ID = ID
  this.生命值 = 42
}

士兵.prototype = {
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

// 保存为文件:士兵.js
然后是创建士兵(加了一个 new 关键字):

var 士兵们 = []
for(var i=0; i<p><strong>new 的作用,就是省那么几行代码。(也就是所谓的语法糖)</strong></p><h4 id="注意-constructor-属性">注意 constructor 属性</h4><p>new 操作为了记录「临时对象是由哪个函数创建的」,所以预先给「士兵.prototype」加了一个 constructor 属性:</p><pre class="brush:php;toolbar:false">士兵.prototype = {
  constructor: 士兵
}

如果你重新对「士兵.prototype」赋值,那么这个 constructor 属性就没了,所以你应该这么写:

士兵.prototype.兵种 = "美国大兵"
士兵.prototype.攻击力 = 5
士兵.prototype.行走 = function(){ /*走俩步的代码*/}
士兵.prototype.奔跑 = function(){ /*狂奔的代码*/  }
士兵.prototype.死亡 = function(){ /*Go die*/    }
士兵.prototype.攻击 = function(){ /*糊他熊脸*/   }
士兵.prototype.防御 = function(){ /*护脸*/       }

或者你也可以自己给 constructor 重新赋值:

士兵.prototype = {
  constructor: 士兵,
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

四、继承

继承的本质就是上面的讲的原型链

1)借助构造函数实现继承

 function Parent1() {
   this.name = 'parent1';
 }
 
 Parent1.prototype.say = function () {}
 
 function Child1() {
   Parent1.call(this);
   this.type = 'child';
 }

 console.log(new Child1);

打印结果:

帶你徹底弄清楚Javascript繼承的原理!

这个主要是借用call 来改变this的指向,通过 call 调用 Parent ,此时 Parent 中的 this 是指 Child1。有个缺点,从打印结果看出 Child1并没有say方法,所以这种只能继承父类的实例属性和方法,不能继承原型属性/方法。

2)借助原型链实现继承

/**
 * 借助原型链实现继承
 */
function Parent2() {
  this.name = 'parent2';
  this.play = [1, 2, 3];
}

function Child2() {
  this.type = 'child2';
}
Child2.prototype = new Parent2();

console.log(new Child2);

var s1 = new Child2();
var s2 = new Child2();

打印:

帶你徹底弄清楚Javascript繼承的原理!

通过一讲的,我们知道要共享莫些属性,需要 对象.__proto__ = 父亲对象的.prototype,但实际上我们是不能直接 操作__proto__,这时我们可以借用 new 来做,所以
Child2.prototype = new Parent2(); Child2.prototype.__proto__ = Parent2.prototype; 这样我们借助  new 这个语法糖,就可以实现原型链继承。但这里有个总是,如打印结果,我们给 s1.play新增一个值 ,s2 也跟着改了。所以这个是原型链继承的缺点,原因是 s1.__pro__ 和 s2.__pro__指向同一个地址即 父类的prototype。

3)组合方式实现继承

/**
 * 组合方式
 */

function Parent3() {
  this.name = 'parent3';
  this.play = [1, 2, 3];
}

Parent3.prototype.say = function () { }

function Child3 () {
  Parent3.call(this);
  this.type = 'child3';
}

Child3.prototype = new Parent3();

var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(new Child3);
console.log(s3.play, s4.play)

打印:

帶你徹底弄清楚Javascript繼承的原理!

将 1 和 2 两种方式组合起来,就可以解决1和2存在问题,这种方式为组合继承。这种方式有点缺点就是我实例一个对象的时, 父类 new 了两次,一次是var s3 = new Child3()对应  Child3.prototype = new Parent3()还要new 一次。

4)组合继承的优化1

function Parent4() {
  this.name = 'parent4';
  this.play = [1, 2, 3];
}

Parent4.prototype.say = function () { }

function Child4() {
  Parent4.call(this);
  this.type = 'child4';
}

Child4.prototype = Parent4.prototype;

var s5 = new Child4();
var s6 = new Child4();

这边主要为 Child4.prototype = Parent4.prototype, 因为我们通过构造函数就可以拿到所有属性和实例的方法,那么现在我想继承父类的原型对象,所以你直接赋值给我就行,不用在去 new 一次父类。其实这种方法还是有问题的,如果我在控制台打印以下两句:

帶你徹底弄清楚Javascript繼承的原理!
从打印可以看出,此时我是没有办法区分一个对象 是直接 由它的子类实例化还是父类呢?我们还有一个方法判断来判断对象是否是类的实例,那就是用 constructor,我在控制台打印以下内容:

帶你徹底弄清楚Javascript繼承的原理!

咦,你会发现它指向的是父类 ,这显然不是我们想要的结果, 上面讲过我们 prototype里面有一个 constructor, 而我们此时子类的 prototype 指向是 父类的 prototye ,而父类prototype里面的contructor当然是父类自己的,这个就是产生该问题的原因。

组合继承的优化2

/**
 * 组合继承的优化2
 */

function Parent5() {
  this.name = 'parent4';
  this.play = [1, 2, 3];
}

Parent5.prototype.say = function () { }

function Child5() {
  Parent5.call(this);
  this.type = 'child4';
}

Child5.prototype = Object.create(Parent5.prototype);

这里主要使用Object.create(),它的作用是将对象继承到__proto__属性上。举个例子:

var test = Object.create({x:123,y:345});
console.log(test);//{}
console.log(test.x);//123
console.log(test.__proto__.x);//3
console.log(test.__proto__.x === test.x);//true

那大家可能说这样解决了吗,其实没有解决,因为这时 Child5.prototype 还是没有自己的 constructor,它要找的话还是向自己的原型对象上找最后还是找到  Parent5.prototype, constructor还是 Parent5 ,所以要给 Child5.prototype 写自己的 constructor:

Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;

以上是帶你徹底弄清楚Javascript繼承的原理!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:segmentfault思否。如有侵權,請聯絡admin@php.cn刪除
Python vs. JavaScript:社區,圖書館和資源Python vs. JavaScript:社區,圖書館和資源Apr 15, 2025 am 12:16 AM

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

從C/C到JavaScript:所有工作方式從C/C到JavaScript:所有工作方式Apr 14, 2025 am 12:05 AM

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript引擎:比較實施JavaScript引擎:比較實施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)