摘要:有了原型鏈,就有了繼承,繼承就是一個對象像繼承遺產一樣繼承從它的構造函數中獲得一些屬性的訪問權。這里其實就是一個原型鏈與繼承的典型例子,開發中可能構造函數復雜一點,屬性定義的多一些,但是原理都是一樣的。
作用域、原型鏈、繼承與閉包詳解
注意:本章講的是在es6之前的原型鏈與繼承。es6引入了類的概念,只是在寫法上有所不同,原理是一樣的。幾個面試常問的幾個問題,你是否知道
instanceof的原理
如何準確判斷變量的類型
如何寫一個原型鏈繼承的例子
描述new一個對象的過程
也許有些同學知道這幾個問題的答案,就會覺得很小兒科,如果你還不知道這幾個問題的答案或者背后所涉及到的知識點,那就好好看完下文,想必對你會有幫助。先不說答案,下面先分析一下涉及到的知識點。什么是構造函數
JavaScript沒有類的概念,JavaScript是一種基于對象的語言,除了五中值類型(number boolean string null undefined)之外,其他的三種引用類型(object、Array、Function)本質上都是對象,而構造函數其實也是普通的函數,只是可以使用構造函數來實例化對象。
事實上,當任意一個普通函數用于創建一類對象時,它就被稱作構造函數。像js的內置函數Object、Array、Date等都是構造函數。
在定義構造函數有以下幾個特點:
以大寫字母開頭定義構造函數
在函數內部對新對象(this)的屬性進行設置
返回值必須是this,或者其它非對象類型的值
下面定義一個簡單的、標準的構造函數:
function Obj(){ this.name = "name" return this // 默認有這一行 } var foo = new Obj() // 使用上面定義的構造函數創建一個對象實例原型特性
js原型有5個特點,記住這5條特點,相信你一定會弄明白長期困擾你的原型關系。
除了null所有引用類型(Object、Array、Function)都有對象特性,也就是都可以自由擴展屬性。
所有引用類型都有一個_proto_屬性(又稱為:隱式屬性),_proto_是一個普通的對象。所有的對象都會有一個constructor屬性,constructor始終指向創建當前對象的構造函數
所有的函數都有一個prototype屬性(又稱為:顯式屬性),也是一個普通對象,這個prototype有一個constructor屬性指向該函數。
所有的引用類型的_proto_屬性指向它的構造函數的prototype屬性(比如:obj._proto_指向Object.prototype,obj是定義的一個普通對象,Object是js的內置函數)
當從一個對象中獲得某個屬性時,如果這個對象沒有該屬性,就會去它的_proto_(也就是它的構造函數的prototype)中去尋找
先來解釋一下這幾條:
第一條的自由擴展性可以通過一個簡單的例子來看
var obj = {} obj.name = "name" console.log(obj) // {name:"name"}
第二條和第三條是javascript就是這么規定的,沒什么好說的
第四條可以這么理解,當定義一個引用類型的變量var obj = {} 其實是var obj = new Object()的語法糖,這樣Object就是obj的構造函數,根據第4條規定,obj._proto_ === Object.prototype,如果不理解可以看看上一章我們講的js內置函數和上面講的構造函數
第五條應該好理解,當從obj中獲取某個屬性時,如果obj中沒有定義該屬性,就會逐級去它的_proto_對象中去尋找,而它的_proto_指向Object的prototype,也就是從Object的prototype對象中去尋找。
原型鏈與繼承如果上面明白了原型,那么原型鏈就會很好理解
根據原型定義的第4條和第5條,很容易發現通過對象的_proto_和函數的prototype把我們變量和構造函數(自定義的構造函數以及內置構造函數)像鏈子一樣鏈接起來,所以又叫他原型鏈。
有了原型鏈,就有了繼承,繼承就是一個對象像繼承遺產一樣繼承從它的構造函數中獲得一些屬性的訪問權。從下面一個小例子理解:
function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; // 原型繼承 function Cat(){ } Cat.prototype = new Animal(); Cat.prototype.name = "cat";
上面例子中在Foo構造函數的prototype中自定義一個somefn函數。然后通過new Foo()創建一個對象實例并賦值給bar變量,此時bar就等于{name:"bar"}。然后bar.somefn就去bar對象中尋找somefn這個屬性,發現找不到,然后就去它的_proto_(其實就是Foo的prototype)中尋找,發現somefn就在Foo的prototype中定義了,就可以愉快的調用并執行somefn了。
這里其實就是一個原型鏈與繼承的典型例子,開發中可能構造函數復雜一點,屬性定義的多一些,但是原理都是一樣的。
留一個問題,根據上面例子,如果執行bar.stString(),應該去哪里找toString這個方法? (提示:prototype也是普通對象,也有自己的_proto_)幾種繼承方式
這幾種都是es5中的繼承,es6中提供了class類,繼承起來更方便。
原型繼承上述例子就是一個原型繼承:
function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; // 原型繼承 function Cat(){ } Cat.prototype = new Animal(); Cat.prototype.name = "cat"; var cat = new Cat() console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true
優點:
非常純粹的繼承關系,實例是子類的實例,也是父類的實例
簡單,易于實現
缺點
要想為子類新增屬性和方法,必須要在new Animal()這樣的語句之后執行,不能放到構造器中
無法實現多繼承
來自原型對象的引用屬性是所有實例共享的(嚴重缺點)
創建子類實例時,無法向父類構造函數傳參(嚴重缺點)
構造繼承function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; // 構造繼承 function Cat(name){ Animal.call(this); this.name = name || "Tom"; } // Test Code var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); // Tom正在睡覺! // console.log(cat.eat("fish")); // cat.eat is not a function console.log(cat instanceof Animal); // false console.log(cat instanceof Cat); // true
優點
解決了1中,子類實例共享父類引用屬性的問題
創建子類實例時,可以向父類傳遞參數
可以實現多繼承
缺點
實例并不是父類的實例,只是子類的實例
只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
無法實現函數復用,每個子類都有父類實例函數的副本,影響性能
實例繼承function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; // 實例繼承 function Cat(name){ var instance = new Animal(); instance.name = name || "Tom"; return instance; } var cat = new Cat(); // 或者可以直接var cat = Cat() console.log(cat.name); console.log(cat.sleep()); // Tom正在睡覺! console.log(cat.eat("fish")); // Tom正在吃:fish console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); // false
優點
不限制調用方式,不管是new Cat()還是Cat(),返回的對象具有相同的效果
缺點
實例是父類的實例,不是子類的實例
不支持多繼承
組合繼承function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; // 組合繼承 function Cat(name){ Animal.call(this); this.name = name || "Tom"; } Cat.prototype = new Animal(); Cat.prototype.constructor = Cat; var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); // Tom正在睡覺! console.log(cat.eat("fish")); // Tom正在吃:fish console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); // true
優點
彌補了方式2的缺陷,可以繼承實例屬性/方法,也可以繼承原型屬性/方法
既是子類的實例,也是父類的實例
不存在引用屬性共享問題
可傳參
函數可復用
缺點
調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
寄生繼承var ob = {name:"小明",friends:["小花","小白"]}; function object(o){ function F(){}//創建一個構造函數F F.prototype = o; return new F(); } //上面再ECMAScript5 有了一新的規范寫法,Object.create(ob) 效果是一樣的 function createOb(o){ var newob = object(o);//創建對象 newob.sayname = function(){//增強對象 console.log(this.name); } return newob;//指定對象 } var ob1 = createOb(ob); ob1.sayname()
寄生繼承原理尚不明白。寄生組合繼承
寄生組合繼承有兩種方式:
第一種:利用創建沒有實例方法的函數
function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; //寄生組合繼承 function Cat(name){ Animal.call(this); this.name = name || "Tom"; } (function(){ // 創建一個沒有實例方法的類 var Super = function(){}; Super.prototype = Animal.prototype; //將實例作為子類的原型 Cat.prototype = new Super(); Cat.prototype.constructor = Cat; })(); var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); // Tom正在睡覺! console.log(cat.eat("fish")); // Tom正在吃:fish console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); //true
第二種:利用Object.create函數
// 寄生繼承核心方法 function inheritPrototype(Parent, Children){ var prototype = Object.create(Parent.prototype); prototype.constructor = Children; Children.prototype = prototype; } // 父類 function Animal (name) { // 屬性 this.name = name || "Animal"; // 實例方法 this.sleep = function(){ console.log(this.name + "正在睡覺!"); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + "正在吃:" + food); }; // 子類 function Cat(name){ Animal.call(this); this.name = name || "Tom"; } inheritPrototype(Animal, Cat) var cat = new Cat(); console.log(cat.name); console.log(cat.sleep()); // Tom正在睡覺! console.log(cat.eat("fish")); // Tom正在吃:fish console.log(cat instanceof Animal); // true console.log(cat instanceof Cat); //true
Object.create其實與以下代碼等價
function object(o){ function f(){} f.prototype = o; return new f(); }
優點
最完美的繼承解決方案
缺點
實現復雜
解答一下最一開始提出的問題看到這里應該對原型鏈與繼承的原理有所了解了,再回頭看上面的問題,你也會發現這都是小兒科。
第一個問題:instanceof原理?
var arr = [] arr instanceof Array
instanceof原理就是利用了原型鏈,當執行arr instanceof Array時,會從arr的_proto_一層一層往上找,看是否能不能找到Array的prototype。
我們知道var arr = [] 其實是var arr = new Array()的語法糖,所以arr的_proto_指向Array的prototype,結果返回true
第二個問題:如何準確判斷變量類型?
可以使用instanceof幫助我們判斷,而不是typeof
第三個問題:如何寫一個原型鏈繼承的例子?
function Foo () { this.name = "name" this.run = function () { console.log(this.name) } } function Bar () {} Bar.prototype = new Foo() // 從構造函數Foo中繼承 var baz = new Bar() baz.run() // 打印出 "name"
第四個問題:描述new一個對象的過程
創建一個新的對象,
獲得構造函數的prototype屬性,并把prototype賦值給新對象的_proto_,this指向這個新對象
執行構造函數,返回構造函數的內容
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/51792.html
摘要:有了原型鏈,就有了繼承,繼承就是一個對象像繼承遺產一樣繼承從它的構造函數中獲得一些屬性的訪問權。這里其實就是一個原型鏈與繼承的典型例子,開發中可能構造函數復雜一點,屬性定義的多一些,但是原理都是一樣的。 作用域、原型鏈、繼承與閉包詳解 注意:本章講的是在es6之前的原型鏈與繼承。es6引入了類的概念,只是在寫法上有所不同,原理是一樣的。 幾個面試常問的幾個問題,你是否知道 insta...
摘要:綜上所述有原型鏈繼承,構造函數繼承經典繼承,組合繼承,寄生繼承,寄生組合繼承五種方法,寄生組合式繼承,集寄生式繼承和組合繼承的優點于一身是實現基于類型繼承的最有效方法。 一、前言 繼承是面向對象(OOP)語言中的一個最為人津津樂道的概念。許多面對對象(OOP)語言都支持兩種繼承方式::接口繼承 和 實現繼承 。 接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。由于js中方法沒有簽名...
摘要:函數式編程前端掘金引言面向對象編程一直以來都是中的主導范式。函數式編程是一種強調減少對程序外部狀態產生改變的方式。 JavaScript 函數式編程 - 前端 - 掘金引言 面向對象編程一直以來都是JavaScript中的主導范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數式編程越來越多得受到開發者的青睞。函數式編程是一種強調減少對程序外部狀態產生改變的方式。因此,...
摘要:忍者級別的函數操作對于什么是匿名函數,這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數是一個很重要且具有邏輯性的特性。通常,匿名函數的使用情況是創建一個供以后使用的函數。 JS 中的遞歸 遞歸, 遞歸基礎, 斐波那契數列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執行機制 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果...
閱讀 1518·2023-04-25 17:41
閱讀 3040·2021-11-22 15:08
閱讀 842·2021-09-29 09:35
閱讀 1605·2021-09-27 13:35
閱讀 3323·2021-08-31 09:44
閱讀 2716·2019-08-30 13:20
閱讀 1939·2019-08-30 13:00
閱讀 2558·2019-08-26 12:12