javascript
面试官问:JS的继承
原文作者若川,掘金鏈接:https://juejin.im/post/5c433e216fb9a049c15f841b
寫于2019年2月20日,現(xiàn)在發(fā)到公眾號聲明原創(chuàng),之前被《前端大全》公眾號等轉(zhuǎn)載閱讀量超1w+,知乎掘金等累計(jì)閱讀量超過1w+。
導(dǎo)讀:文章主要通過ES6的extends,結(jié)合ES5的寄生組合繼承,圖文并茂的講述JS的繼承,最后還推薦了一些書籍的繼承的章節(jié),旨在讓讀者掌握J(rèn)S的繼承。
用過?React的讀者知道,經(jīng)常用?extends繼承?React.Component。
// 部分源碼 function Component(props, context, updater) {// ... } Component.prototype.setState = function(partialState, callback){// ... } const React = {Component,// ... } // 使用 class index extends React.Component{// ... }點(diǎn)擊這里查看 React github源碼
面試官可以順著這個(gè)問?JS繼承的相關(guān)問題,比如:?ES6的?class繼承用ES5如何實(shí)現(xiàn)。據(jù)說很多人答得不好。
構(gòu)造函數(shù)、原型對象和實(shí)例之間的關(guān)系
要弄懂extends繼承之前,先來復(fù)習(xí)一下構(gòu)造函數(shù)、原型對象和實(shí)例之間的關(guān)系。代碼表示:
function F(){} var f = new F(); // 構(gòu)造器 F.prototype.constructor === F; // true F.__proto__ === Function.prototype; // true Function.prototype.__proto__ === Object.prototype; // true Object.prototype.__proto__ === null; // true // 實(shí)例 f.__proto__ === F.prototype; // true F.prototype.__proto__ === Object.prototype; // true Object.prototype.__proto__ === null; // true筆者畫了一張圖表示:?
ES6extends?繼承做了什么操作
我們先看看這段包含靜態(tài)方法的?ES6繼承代碼:
// ES6 class Parent{constructor(name){this.name = name;}static sayHello(){console.log('hello');}sayName(){console.log('my name is ' + this.name);return this.name;} } class Child extends Parent{constructor(name, age){super(name);this.age = age;}sayAge(){console.log('my age is ' + this.age);return this.age;} } let parent = new Parent('Parent'); let child = new Child('Child', 18); console.log('parent: ', parent); // parent: Parent?{name: "Parent"} Parent.sayHello(); // hello parent.sayName(); // my name is Parent console.log('child: ', child); // child: Child?{name: "Child", age: 18} Child.sayHello(); // hello child.sayName(); // my name is Child child.sayAge(); // my age is 18其中這段代碼里有兩條原型鏈,不信看具體代碼。
// 1、構(gòu)造器原型鏈 Child.__proto__ === Parent; // true Parent.__proto__ === Function.prototype; // true Function.prototype.__proto__ === Object.prototype; // true Object.prototype.__proto__ === null; // true // 2、實(shí)例原型鏈 child.__proto__ === Child.prototype; // true Child.prototype.__proto__ === Parent.prototype; // true Parent.prototype.__proto__ === Object.prototype; // true Object.prototype.__proto__ === null; // true一圖勝千言,筆者也畫了一張圖表示,如圖所示:
?結(jié)合代碼和圖可以知道。?ES6extends?繼承,主要就是:
1.把子類構(gòu)造函數(shù)(?Child)的原型(?__proto__)指向了父類構(gòu)造函數(shù)(?Parent),
2.把子類實(shí)例?child的原型對象(?Child.prototype) 的原型(?__proto__)指向了父類?parent的原型對象(?Parent.prototype)。
這兩點(diǎn)也就是圖中用不同顏色標(biāo)記的兩條線。
3.子類構(gòu)造函數(shù)?Child繼承了父類構(gòu)造函數(shù)?Preant的里的屬性。使用?super調(diào)用的(?ES5則用?call或者?apply調(diào)用傳參)。也就是圖中用不同顏色標(biāo)記的兩條線。
看過《JavaScript高級程序設(shè)計(jì)-第3版》 章節(jié)?6.3繼承的讀者應(yīng)該知道,這?2和3小點(diǎn),正是寄生組合式繼承,書中例子沒有?第1小點(diǎn)。?1和2小點(diǎn)都是相對于設(shè)置了?__proto__鏈接。那問題來了,什么可以設(shè)置了?__proto__鏈接呢。
new、?Object.create和?Object.setPrototypeOf可以設(shè)置?__proto__
說明一下,?__proto__這種寫法是瀏覽器廠商自己的實(shí)現(xiàn)。再結(jié)合一下圖和代碼看一下的?new,?new出來的實(shí)例的proto指向構(gòu)造函數(shù)的?prototype,這就是?new做的事情。摘抄一下之前寫過文章的一段。面試官問:能否模擬實(shí)現(xiàn)JS的new操作符,有興趣的讀者可以點(diǎn)擊查看。
new做了什么:
創(chuàng)建了一個(gè)全新的對象。
這個(gè)對象會被執(zhí)行?[[Prototype]](也就是?__proto__)鏈接。
生成的新對象會綁定到函數(shù)調(diào)用的?this。
通過?new創(chuàng)建的每個(gè)對象將最終被?[[Prototype]]鏈接到這個(gè)函數(shù)的?prototype對象上。
如果函數(shù)沒有返回對象類型?Object(包含?Functoin,?Array,?Date,?RegExg,?Error),那么?new表達(dá)式中的函數(shù)調(diào)用會自動返回這個(gè)新的對象。
Object.create?ES5提供的
Object.create(proto,[propertiesObject])?方法創(chuàng)建一個(gè)新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的proto。它接收兩個(gè)參數(shù),不過第二個(gè)可選參數(shù)是屬性描述符(不常用,默認(rèn)是?undefined)。對于不支持?ES5的瀏覽器,?MDN上提供了?ployfill方案。?MDN Object.create()
// 簡版:也正是應(yīng)用了new會設(shè)置__proto__鏈接的原理。 if(typeof Object.create !== 'function'){Object.create = function(proto){function F() {}F.prototype = proto;return new F();} }Object.setPrototypeOf?ES6提供的
Object.setPrototypeOf?MDN
Object.setPrototypeOf()?方法設(shè)置一個(gè)指定的對象的原型 ( 即, 內(nèi)部?[[Prototype]]屬性)到另一個(gè)對象或?null。?Object.setPrototypeOf(obj,prototype)
`ployfill` // 僅適用于Chrome和FireFox,在IE中不工作: Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {obj.__proto__ = proto;return obj; }nodejs源碼就是利用這個(gè)實(shí)現(xiàn)繼承的工具函數(shù)的。?nodejs utils inherits
function inherits(ctor, superCtor) {if (ctor === undefined || ctor === null)throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);if (superCtor === undefined || superCtor === null)throw new ERR_INVALID_ARG_TYPE('superCtor', 'Function', superCtor);if (superCtor.prototype === undefined) {throw new ERR_INVALID_ARG_TYPE('superCtor.prototype','Object', superCtor.prototype);}Object.defineProperty(ctor, 'super_', {value: superCtor,writable: true,configurable: true});Object.setPrototypeOf(ctor.prototype, superCtor.prototype); }ES6的?extends的?ES5版本實(shí)現(xiàn)
知道了?ES6extends繼承做了什么操作和設(shè)置?__proto__的知識點(diǎn)后,把上面?ES6例子的用?ES5就比較容易實(shí)現(xiàn)了,也就是說實(shí)現(xiàn)寄生組合式繼承,簡版代碼就是:
// ES5 實(shí)現(xiàn)ES6 extends的例子 function Parent(name){this.name = name; } Parent.sayHello = function(){console.log('hello'); } Parent.prototype.sayName = function(){console.log('my name is ' + this.name);return this.name; } function Child(name, age){// 相當(dāng)于superParent.call(this, name);this.age = age; } // new function object(){function F() {}F.prototype = proto;return new F(); } function _inherits(Child, Parent){// Object.createChild.prototype = Object.create(Parent.prototype);// __proto__// Child.prototype.__proto__ = Parent.prototype;Child.prototype.constructor = Child;// ES6// Object.setPrototypeOf(Child, Parent);// __proto__Child.__proto__ = Parent; } _inherits(Child, Parent); Child.prototype.sayAge = function(){console.log('my age is ' + this.age);return this.age; } var parent = new Parent('Parent'); var child = new Child('Child', 18); console.log('parent: ', parent); // parent: Parent?{name: "Parent"} Parent.sayHello(); // hello parent.sayName(); // my name is Parent console.log('child: ', child); // child: Child?{name: "Child", age: 18} Child.sayHello(); // hello child.sayName(); // my name is Child child.sayAge(); // my age is 18我們完全可以把上述?ES6的例子通過?babeljs轉(zhuǎn)碼成?ES5來查看,更嚴(yán)謹(jǐn)?shù)膶?shí)現(xiàn)。
// 對轉(zhuǎn)換后的代碼進(jìn)行了簡要的注釋 "use strict"; // 主要是對當(dāng)前環(huán)境支持Symbol和不支持Symbol的typeof處理 function _typeof(obj) {if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {_typeof = function _typeof(obj) {return typeof obj;};} else {_typeof = function _typeof(obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;};}return _typeof(obj); } // _possibleConstructorReturn 判斷Parent。call(this, name)函數(shù)返回值 是否為null或者函數(shù)或者對象。 function _possibleConstructorReturn(self, call) {if (call && (_typeof(call) === "object" || typeof call === "function")) {return call;}return _assertThisInitialized(self); } // 如何 self 是void 0 (undefined) 則報(bào)錯(cuò) function _assertThisInitialized(self) {if (self === void 0) {throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return self; } // 獲取__proto__ function _getPrototypeOf(o) {_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {return o.__proto__ || Object.getPrototypeOf(o);};return _getPrototypeOf(o); } // 寄生組合式繼承的核心 function _inherits(subClass, superClass) {if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function");}// Object.create()方法創(chuàng)建一個(gè)新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的__proto__。// 也就是說執(zhí)行后 subClass.prototype.__proto__ === superClass.prototype; 這條語句為truesubClass.prototype = Object.create(superClass && superClass.prototype, {constructor: {value: subClass,writable: true,configurable: true}});if (superClass) _setPrototypeOf(subClass, superClass); } // 設(shè)置__proto__ function _setPrototypeOf(o, p) {_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {o.__proto__ = p;return o;};return _setPrototypeOf(o, p); } // instanceof操作符包含對Symbol的處理 function _instanceof(left, right) {if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {return right[Symbol.hasInstance](left);} else {return left instanceof right;} } function _classCallCheck(instance, Constructor) {if (!_instanceof(instance, Constructor)) {throw new TypeError("Cannot call a class as a function");} } // 按照它們的屬性描述符 把方法和靜態(tài)屬性賦值到構(gòu)造函數(shù)的prototype和構(gòu)造器函數(shù)上 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);} } // 把方法和靜態(tài)屬性賦值到構(gòu)造函數(shù)的prototype和構(gòu)造器函數(shù)上 function _createClass(Constructor, protoProps, staticProps) {if (protoProps) _defineProperties(Constructor.prototype, protoProps);if (staticProps) _defineProperties(Constructor, staticProps);return Constructor; } // ES6 var Parent = function () {function Parent(name) {_classCallCheck(this, Parent);this.name = name;}_createClass(Parent, [{key: "sayName",value: function sayName() {console.log('my name is ' + this.name);return this.name;}}], [{key: "sayHello",value: function sayHello() {console.log('hello');}}]);return Parent; }(); var Child = function (_Parent) {_inherits(Child, _Parent);function Child(name, age) {var _this;_classCallCheck(this, Child);// Child.__proto__ => Parent// 所以也就是相當(dāng)于Parent.call(this, name); 是super(name)的一種轉(zhuǎn)換// _possibleConstructorReturn 判斷Parent.call(this, name)函數(shù)返回值 是否為null或者函數(shù)或者對象。_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));_this.age = age;return _this;}_createClass(Child, [{key: "sayAge",value: function sayAge() {console.log('my age is ' + this.age);return this.age;}}]);return Child; }(Parent); var parent = new Parent('Parent'); var child = new Child('Child', 18); console.log('parent: ', parent); // parent: Parent?{name: "Parent"} Parent.sayHello(); // hello parent.sayName(); // my name is Parent console.log('child: ', child); // child: Child?{name: "Child", age: 18} Child.sayHello(); // hello child.sayName(); // my name is Child child.sayAge(); // my age is 18如果對JS繼承相關(guān)還是不太明白的讀者,推薦閱讀以下書籍的相關(guān)章節(jié),可以自行找到相應(yīng)的?pdf版本。
推薦閱讀JS繼承相關(guān)的書籍章節(jié)
《JavaScript高級程序設(shè)計(jì)第3版》-第6章 面向?qū)ο蟮某绦蛟O(shè)計(jì),6種繼承的方案,分別是原型鏈繼承、借用構(gòu)造函數(shù)繼承、組合繼承、原型式繼承、寄生式繼承、寄生組合式繼承。圖靈社區(qū)本書地址,后文放出?github鏈接,里面包含這幾種繼承的代碼?demo。
《JavaScript面向?qū)ο缶幊痰?版》-第6章 繼承,12種繼承的方案。1.原型鏈法(仿傳統(tǒng))、2.僅從原型繼承法、3.臨時(shí)構(gòu)造器法、4.原型屬性拷貝法、5.全屬性拷貝法(即淺拷貝法)、6.深拷貝法、7.原型繼承法、8.擴(kuò)展與增強(qiáng)模式、9.多重繼承法、10.寄生繼承法、11.構(gòu)造器借用法、12.構(gòu)造器借用與屬性拷貝法。
ES6標(biāo)準(zhǔn)入門-第21章class的繼承
《深入理解?ES6》-第9章?JavaScript中的類
《你不知道的?JavaScript-上卷》第6章 行為委托和附錄A?ES6中的class
總結(jié)
繼承對于JS來說就是父類擁有的方法和屬性、靜態(tài)方法等,子類也要擁有。子類中可以利用原型鏈查找,也可以在子類調(diào)用父類,或者從父類拷貝一份到子類等方案。繼承方法可以有很多,重點(diǎn)在于必須理解并熟 悉這些對象、原型以及構(gòu)造器的工作方式,剩下的就簡單了。寄生組合式繼承是開發(fā)者使用比較多的。回顧寄生組合式繼承。主要就是三點(diǎn):
1.子類構(gòu)造函數(shù)的?__proto__指向父類構(gòu)造器,繼承父類的靜態(tài)方法。
2.子類構(gòu)造函數(shù)的?prototype的?__proto__指向父類構(gòu)造器的?prototype,繼承父類的方法。
3.子類構(gòu)造器里調(diào)用父類構(gòu)造器,繼承父類的屬性。行文到此,文章就基本寫完了。文章代碼和圖片等資源放在這里github inhert和?demo展示?es6-extends,結(jié)合?console、source面板查看更佳。
讀者發(fā)現(xiàn)有不妥或可改善之處,歡迎評論指出。另外覺得寫得不錯(cuò),可以點(diǎn)贊、評論、轉(zhuǎn)發(fā),也是對筆者的一種支持。
關(guān)于
作者:常以若川為名混跡于江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學(xué)。
個(gè)人博客 http://lxchuan12.github.io?使用?vuepress重構(gòu)了,閱讀體驗(yàn)可能更好些
https://github.com/lxchuan12/blog,相關(guān)源碼和資源都放在這里,求個(gè) star^_^~
微信交流群,加我微信lxchuan12,注明來源,拉您進(jìn)前端視野交流群
下圖是公眾號二維碼:若川視野,一個(gè)可能比較有趣的前端開發(fā)類公眾號,目前前端內(nèi)容不多
往期文章
工作一年后,我有些感悟(寫于2017年)
高考七年后、工作三年后的感悟
學(xué)習(xí) jQuery 源碼整體架構(gòu),打造屬于自己的 js 類庫
學(xué)習(xí)underscore源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫
學(xué)習(xí) lodash 源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫
由于公眾號限制外鏈,點(diǎn)擊閱讀原文,或許閱讀體驗(yàn)更佳,覺得文章不錯(cuò),可以點(diǎn)個(gè)在看呀^_^
總結(jié)
以上是生活随笔為你收集整理的面试官问:JS的继承的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于stc15f2k60s2芯片单片机编
- 下一篇: 前端学习(3104):react-hel