摘要:它繼承自構造函數被執行,相應的參數會被傳入,同時上下文會指向這個新的實例除非明確返回值,否則返回新的實例至此,我們實現了里面的類對象和屬性的概念,和有相同的屬性,但是值并不相同即屬性是私有的。
眾所周知,JS并沒有類(class)的概念,雖然說ES6開始有了類的概念,但是,這并不是說JS有了像Ruby、Java這些基于類的面向對象語言一樣,有了全新的繼承模型。ES6中的類,僅僅只是基于現有的原型繼承的一種語法糖,下面我們好好分析一下,具體是如何實現的
面向對象思想在講正題之前,我們先來討論一下各種面試題都可能出現的一個問題,什么是面向對象編程(OOP)?
類:定義某一事物的抽象特點,包含屬性和方法,舉個栗子,狗這個類包含狗的一些基礎特征,如毛皮顏色,吠叫等能力。
對象:類的一個實例,還是舉個栗子,小明家的白色的狗和小紅家紅色的狗。
屬性:對象的特征,比如剛提到的狗皮毛的顏色。
方法:對象的行為,比如剛才提到的狗的吠叫能力。
封裝性:通過限制只有特定類的對象可以訪問特定類的成員,一般包含public protected private 三種,不同語言的實現不同。
繼承性:一個類會有子類,這個子類是更具體化的一個抽象,它包含父類的一些屬性和方法,并且有可能有不同于父類的屬性和方法。
多態性:多意為‘許多’,態意為‘形態’。不同類可以定義相同的方法或屬性。
抽象性:復雜現實問題轉化為類定義的途徑,包括以上所有內容。
如何實現對象(類)的定義由于JS并沒有類(class)的概念,更多的時候我們把它叫做對象(function),然后把對象叫做實例(instance),跟團隊里面的人討論OOP的時候,經常會有概念上的一些誤解,特此說明一下。
構造函數:一個指明了對象類型的函數,通常我們可以通過構造函數類創建在js里面,我們通常都是通過構造函數來創建對象(class),然后通過new這個關鍵字來實例化一個對象,如:
function Dog(name){ this.name = name; } var d1 = new Dog("dodo"); d1.constructor // Dog(name){ // this.name = name; // } var d2 = new Dog("do2do");
為什么通過構造函數可以實現對象(class)屬性的定義呢?首先,我們必須理解這個語法new constructor[([arguments])]
我們來具體看看當new Dog("name")時,具體做了哪些事情
一個新實例被創建。它繼承自Dog.prototype
構造函數被執行,相應的參數會被傳入,同時上下文(this)會指向這個新的實例
除非明確返回值,否則返回新的實例
至此,我們實現了OOP里面的類(Dog)、對象(d1,d2)、和屬性(name)的概念,d1和d2有相同的name屬性,但是值并不相同,即屬性是私有的。
原型對象(prototype)注: 新創建的實例,都包含一個constructor屬性,該屬性指向他們的構造函數Dog
接下來,我們即將討論如何定義方法,其實,我們完全可以這樣定義我們的方法,如:
function Dog(name){ this.name = name; this.bark = function(){ console.log(this.name + " bark"); }; } var d1 = new Dog("dodo"); d1.bark(); // dodo bark
但是,一般我們不推薦這么做,正如我們所知Dog是一個構造函數,每次實例化時,都會執行這個函數,也就是說,bark 這個方法每次都會被定義, 比較浪費內存。但是我們通常可以用constructor和閉包的方式來實現私有屬性,如:
function Dog(name){ this.name = name; // barkCount 是私有屬性,因為實例并不知道這個屬性 var barkCount = 0; this.bark = function(){ barkCount ++; console.log(this.name + " bark"); }; this.getBarkCount = function(){ console.log(this.name + " has barked " + barkCount + " times"); }; } var d1 = new Dog("dodo"); d1.bark(); d1.bark(); d1.getBarkCount(); // dodo has barked 2 times
好像扯得有點遠,我們回歸我們的主角prototype,函數Dog有一個特殊的屬性,這個屬性就叫原型,如上所述,當用new運算符創建實例時,會把Dog的原型對象的引用復制到新的實例內部的[[Prototype]]屬性,即d1.[[Prototype]] = Dog.prototype,因為所有的實例的[[Prototype]]都指向Dog的原型對象,那么,我們就可以很方便的定義我們的方法了,如:
function Dog(name){ this.name = name; } Dog.prototype = { bark: function(){ console.log(this.name + " bark"); } }; var d1 = new Dog("dodo"); d1.bark(); // dodo bark
我們可以通過d1.__proto__ == Dog.prototype,來驗證我們的想法。用原型對象還有一個好處,由于實例化的對象的[[Prototype]]指向Dog的原型對象,那么我們可以通過添加Dog的原型對象的方法,來添加已經實例化后的實例d1的方法。如:
Dog.prototype.run = function(){ console.log(this.name + " is running!"); } d1.run(); // dodo is running!
原型鏈注:所有對象的__proto__都指向其構造器的prototype
上面已經描述如何定義一個類,接下來我們將要了解,如何實現類的繼承。在此之前,我們先了解js里一個老生常談的概念:原型鏈:每個對象都有一個指向它的原型(prototype)對象的內部鏈接。這個原型對象又有自己的原型,直到某個對象的原型為 null 為止(也就是不再有原型指向),組成這條鏈的最后一環。這種一級一級的鏈結構就稱為原型鏈
mozilla給出一個挺好的例子:
// 假定有一個對象 o, 其自身的屬性(own properties)有 a 和 b: // {a: 1, b: 2} // o 的原型 o.[[Prototype]]有屬性 b 和 c: // {b: 3, c: 4} // 最后, o.[[Prototype]].[[Prototype]] 是 null. // 這就是原型鏈的末尾,即 null, // 根據定義,null 沒有[[Prototype]]. // 綜上,整個原型鏈如下: // {a:1, b:2} ---> {b:3, c:4} ---> null console.log(o.a); // 1 // a是o的自身屬性嗎?是的,該屬性的值為1 console.log(o.b); // 2 // b是o的自身屬性嗎?是的,該屬性的值為2 // o.[[Prototype]]上還有一個"b"屬性,但是它不會被訪問到.這種情況稱為"屬性遮蔽 (property shadowing)". console.log(o.c); // 4 // c是o的自身屬性嗎?不是,那看看o.[[Prototype]]上有沒有. // c是o.[[Prototype]]的自身屬性嗎?是的,該屬性的值為4 console.log(o.d); // undefined // d是o的自身屬性嗎?不是,那看看o.[[Prototype]]上有沒有. // d是o.[[Prototype]]的自身屬性嗎?不是,那看看o.[[Prototype]].[[Prototype]]上有沒有. // o.[[Prototype]].[[Prototype]]為null,停止搜索, // 沒有d屬性,返回undefined
現在我們可以通過我們理解的構造函數和原型對象來實現繼承的概念了,代碼如下:
function Dog(name){ this.name = name; } // 這種寫法會修改dog實例的constructor,可以通過Dog.prototype.constructor = Dog來重置 Dog.prototype = { bark: function(){ console.log(this.name + " bark"); } }; // 重置Dog實例的構造函數為本身 Dog.prototype.constructor = Dog; // Haski 的構造函數 function Haski(name){ // 繼承Dog的構造函數 Dog.call(this, name); // 可以補充更多Haski的屬性 this.type = "Haski"; }; // 1. 設置Haski的prototype為Dog的實例對象 // 2. 此時Haski的原型鏈是 Haski -> Dog的實例 -> Dog -> Object // 3. 此時,Haski包含了Dog的所有屬性和方法,而且還有一個指針,指向Dog的原型對象 // 4. 這種做法是不推薦的,下面會改進 Haski.prototype = new Dog(); // 重置Haski實例的構造函數為本身 Haski.prototype.constructor = Haski; // 可以為子類添加更多的方法 Haski.prototype.say = function(){ console.log("I"m " + this.name); } var ha = new Haski("Ha"); // Ha bark ha.bark(); // Ha bark ha.say(); // I"m Ha
注: 子類在定義prototype時,不可直接使用Haski.prototype = {}定義,這樣會重寫Haski的原型鏈,把Haski的原型當做Object的實例,而非Dog的實例
但是,當我想找一下ha的原型鏈時,會發現ha的原型對象指向的是Dog的實例,而且還有一個值為undefined的name屬性,在實例化時,name是沒必要的, 如下圖:
所以,我們需要修改一下我們的實現,代碼如下:
// 修改前 Haski.prototype = new Dog(); // 修改后 Haski.prototype = Object.create(Dog.prototype);
注: __proto__ 方法已棄用,從 ECMAScript 6 開始, [[Prototype]] 可以用Object.getPrototypeOf()和Object.setPrototypeOf()訪問器來訪問
自此,我們已經實現繼承的概念,父類有自己的方法,子類繼承了父類的屬性和方法,而且還可以定義自己的屬性和方法。
ES6 如何實現"use strict"; // 聲明 Dog 類 class Dog { // 構造函數 constructor(name){ this.name = name; } // 普通方法 dark(){ console.log(this.name + "bark"); } // 靜態方法,也叫類方法 static staticMethod(){ console.log("I"m static method!"); } } // 通過`extends`關鍵字來實現繼承 class Haski extends Dog { constructor(name){ // 調用父類的構造函數 super(name); this.type = "Haski"; } // 定義子類方法 say(){ console.log("I"m" + this.name); } }
在ES6中,我們只需通過class extends super constructor 即可比較方便的完成原來使用JS比較難理解的實現,我們可以通過babel的解析器,來看看babel是怎么把這些語法糖轉成JS的實現的。具體代碼可以參考
"use strict"; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn"t been initialised - super() hasn"t been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // 聲明 Dog 類 var Dog = function () { // 構造函數 function Dog(name) { _classCallCheck(this, Dog); this.name = name; } // 普通方法 _createClass(Dog, [{ key: "dark", value: function dark() { console.log(this.name + "bark"); } // 靜態方法,也叫類方法 }], [{ key: "staticMethod", value: function staticMethod() { console.log("I"m static method!"); } }]); return Dog; }(); // 通過`extends`關鍵字來實現繼承 var Haski = function (_Dog) { _inherits(Haski, _Dog); function Haski(name) { _classCallCheck(this, Haski); var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Haski).call(this, name)); // 調用父類的構造函數 _this.type = "Haski"; return _this; } _createClass(Haski, [{ key: "say", value: function say() { console.log("I"m" + this.name); } }]); return Haski; }(Dog);
參考文獻教是最好的學,我正在嘗試把我自己理解的內容分享出來,希望我能講清楚,如果描述有誤,歡迎指正。
Introduction to Object-Oriented JavaScript
Classes
Declaring javascript object method in constructor function vs. in prototype
Inheritance and the prototype chain
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107009.html
摘要:個人前端文章整理從最開始萌生寫文章的想法,到著手開始寫,再到現在已經一年的時間了,由于工作比較忙,更新緩慢,后面還是會繼更新,現將已經寫好的文章整理一個目錄,方便更多的小伙伴去學習。 showImg(https://segmentfault.com/img/remote/1460000017490740?w=1920&h=1080); 個人前端文章整理 從最開始萌生寫文章的想法,到著手...
摘要:可以通過構造函數和原型的方式模擬實現類的功能。原型式繼承與類式繼承類式繼承是在子類型構造函數的內部調用超類型的構造函數。寄生式繼承這種繼承方式是把原型式工廠模式結合起來,目的是為了封裝創建的過程。 js繼承的概念 js里常用的如下兩種繼承方式: 原型鏈繼承(對象間的繼承) 類式繼承(構造函數間的繼承) 由于js不像java那樣是真正面向對象的語言,js是基于對象的,它沒有類的概念。...
摘要:語法父類名表示當前類繼承于哪個類的標簽。成員標簽成員標簽作用于類中的配置屬性函數事件。表明可被子類繼承,和一起使用。示例獲取圓的面積圓的半徑面積值作用于函數,表明函數的標簽。作用于函數,表明構造函數參數的標簽,用法同。 字數:3692字 閱讀時間:15分鐘 前言 ? 首先,咱們有一個前提,JSDuck對我們而言只是一個便于API查看的文檔化工具。因此,只要它能夠滿足我們文...
摘要:選擇器大致可以分成類基本選擇器,層次選擇器,屬性選擇器,偽類,偽元素。但偽類和偽元素相對比較抽象,稍微有一點點理解上的難度。本篇就是我對偽類和偽元素的理解。 CSS選擇器大致可以分成5類:基本選擇器,層次選擇器,屬性選擇器,偽類,偽元素。基本,層次,屬性選擇器比較容易理解,畢竟它們選擇的對象都屬于DOM中看得見摸得著的元素。但偽類和偽元素相對比較抽象,稍微有一點點理解上的難度。本篇就是...
閱讀 1993·2021-08-11 11:13
閱讀 1015·2021-07-25 21:37
閱讀 2577·2019-08-29 18:42
閱讀 2510·2019-08-26 12:18
閱讀 915·2019-08-26 11:29
閱讀 1684·2019-08-23 17:17
閱讀 2664·2019-08-23 15:55
閱讀 2598·2019-08-23 14:34