前言
這是面試官問系列的第五篇,旨在幫助讀者提升
JS
基礎知識,包含new、call、apply、this、繼承
相關知識。
面試官問系列
文章如下:感興趣的讀者可以點選閱讀。
1.面試官問:能否模擬實現JS的new操作符
2.面試官問:能否模擬實現JS的bind方法
3.面試官問:能否模擬實現JS的call和apply方法
4.面試官問:JS的this指向
5.面試官問:JS的繼承
用過React
的讀者知道,經常用extends
繼承React.Component
。
// 部分原始碼
function Component(props, context, updater) {
// ...
}
Component.prototype.setState = function(partialState, callback){
// ...
}
const React = {
Component,
// ...
}
// 使用
class index extends React.Component{
// ...
}
複製程式碼
面試官可以順著這個問JS
繼承的相關問題,比如:ES6
的class
繼承用ES5如何實現。據說很多人答得不好。
建構函式、原型物件和例項之間的關係
要弄懂extends繼承之前,先來複習一下建構函式、原型物件和例項之間的關係。 程式碼表示:
function F(){}
var f = new F();
// 構造器
F.prototype.constructor === F; // true
F.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 例項
f.__proto__ === F.prototype; // true
F.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
複製程式碼
筆者畫了一張圖表示:
ES6 extends
繼承做了什麼操作
我們先看看這段包含靜態方法的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、構造器原型鏈
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、例項原型鏈
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
複製程式碼
一圖勝千言,筆者也畫了一張圖表示,如圖所示:
結合程式碼和圖可以知道。ES6 extends
繼承,主要就是:
-
- 把子類建構函式(
Child
)的原型(__proto__
)指向了父類建構函式(Parent
),
- 把子類建構函式(
-
- 把子類例項
child
的原型物件(Child.prototype
) 的原型(__proto__
)指向了父類parent
的原型物件(Parent.prototype
)。
- 把子類例項
這兩點也就是圖中用不同顏色標記的兩條線。
-
- 子類建構函式
Child
繼承了父類建構函式Preant
的裡的屬性。使用super
呼叫的(ES5
則用call
或者apply
呼叫傳參)。 也就是圖中用不同顏色標記的兩條線。
- 子類建構函式
看過《JavaScript高階程式設計-第3版》 章節6.3繼承
的讀者應該知道,這2和3小點
,正是寄生組合式繼承,書中例子沒有第1小點
。
1和2小點
都是相對於設定了__proto__
連結。那問題來了,什麼可以設定了__proto__
連結呢。
new
、Object.create
和Object.setPrototypeOf
可以設定__proto__
說明一下,__proto__
這種寫法是瀏覽器廠商自己的實現。
再結合一下圖和程式碼看一下的new
,new
出來的例項的__proto__指向建構函式的prototype
,這就是new
做的事情。
摘抄一下之前寫過文章的一段。面試官問:能否模擬實現JS的new操作符,有興趣的讀者可以點選檢視。
new
做了什麼:
- 建立了一個全新的物件。
- 這個物件會被執行
[[Prototype]]
(也就是__proto__
)連結。- 生成的新物件會繫結到函式呼叫的
this
。- 通過
new
建立的每個物件將最終被[[Prototype]]
連結到這個函式的prototype
物件上。- 如果函式沒有返回物件型別
Object
(包含Functoin
,Array
,Date
,RegExg
,Error
),那麼new
表示式中的函式呼叫會自動返回這個新的物件。
Object.create
ES5提供的
Object.create(proto, [propertiesObject])
方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。
它接收兩個引數,不過第二個可選引數是屬性描述符(不常用,預設是undefined
)。對於不支援ES5
的瀏覽器,MDN
上提供了ployfill
方案。
MDN Object.create()
// 簡版:也正是應用了new會設定__proto__連結的原理。
if(typeof Object.create !== 'function'){
Object.create = function(proto){
function F() {}
F.prototype = proto;
return new F();
}
}
複製程式碼
Object.setPrototypeOf
ES6提供的
Object.setPrototypeOf()
方法設定一個指定的物件的原型 ( 即, 內部[[Prototype]]
屬性)到另一個物件或 null
。
Object.setPrototypeOf(obj, prototype)
`ployfill`
// 僅適用於Chrome和FireFox,在IE中不工作:
Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
複製程式碼
nodejs
原始碼就是利用這個實現繼承的工具函式的。
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
版本實現
知道了ES6 extends
繼承做了什麼操作和設定__proto__
的知識點後,把上面ES6
例子的用ES5
就比較容易實現了,也就是說實現寄生組合式繼承,簡版程式碼就是:
// ES5 實現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){
// 相當於super
Parent.call(this, name);
this.age = age;
}
// new
function object(){
function F() {}
F.prototype = proto;
return new F();
}
function _inherits(Child, Parent){
// Object.create
Child.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
轉碼成ES5
來檢視,更嚴謹的實現。
// 對轉換後的程式碼進行了簡要的註釋
"use strict";
// 主要是對當前環境支援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)函式返回值 是否為null或者函式或者物件。
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
// 如何 self 是void 0 (undefined) 則報錯
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()方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。
// 也就是說執行後 subClass.prototype.__proto__ === superClass.prototype; 這條語句為true
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
// 設定__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");
}
}
// 按照它們的屬性描述符 把方法和靜態屬性賦值到建構函式的prototype和構造器函式上
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);
}
}
// 把方法和靜態屬性賦值到建構函式的prototype和構造器函式上
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
// 所以也就是相當於Parent.call(this, name); 是super(name)的一種轉換
// _possibleConstructorReturn 判斷Parent.call(this, name)函式返回值 是否為null或者函式或者物件。
_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繼承相關還是不太明白的讀者,推薦閱讀以下書籍的相關章節,可以自行找到相應的pdf
版本。
推薦閱讀JS繼承相關的書籍章節
《JavaScript高階程式設計第3版》-第6章 物件導向的程式設計,6種繼承的方案,分別是原型鏈繼承、借用建構函式繼承、組合繼承、原型式繼承、寄生式繼承、寄生組合式繼承。圖靈社群本書地址,後文放出github
連結,裡面包含這幾種繼承的程式碼demo
。
《JavaScript物件導向程式設計第2版》-第6章 繼承,12種繼承的方案。1.原型鏈法(仿傳統)、2.僅從原型繼承法、3.臨時構造器法、4.原型屬性拷貝法、5.全屬性拷貝法(即淺拷貝法)、6.深拷貝法、7.原型繼承法、8.擴充套件與增強模式、9.多重繼承法、10.寄生繼承法、11.構造器借用法、12.構造器借用與屬性拷貝法。
《深入理解ES6
》-第9章 JavaScript
中的類
《你不知道的JavaScript
-上卷》第6章 行為委託和附錄A ES6中的class
總結
繼承對於JS來說就是父類擁有的方法和屬性、靜態方法等,子類也要擁有。子類中可以利用原型鏈查詢,也可以在子類呼叫父類,或者從父類拷貝一份到子類等方案。 繼承方法可以有很多,重點在於必須理解並熟 悉這些物件、原型以及構造器的工作方式,剩下的就簡單了。寄生組合式繼承是開發者使用比較多的。 回顧寄生組合式繼承。主要就是三點:
-
- 子類建構函式的
__proto__
指向父類構造器,繼承父類的靜態方法
- 子類建構函式的
-
- 子類建構函式的
prototype
的__proto__
指向父類構造器的prototype
,繼承父類的方法。
- 子類建構函式的
-
- 子類構造器裡呼叫父類構造器,繼承父類的屬性。
行文到此,文章就基本寫完了。文章程式碼和圖片等資源放在這裡github inhert和
demo
展示es6-extends
,結合console、source
皮膚檢視更佳。
- 子類構造器裡呼叫父類構造器,繼承父類的屬性。
行文到此,文章就基本寫完了。文章程式碼和圖片等資源放在這裡github inhert和
讀者發現有不妥或可改善之處,歡迎評論指出。另外覺得寫得不錯,可以點贊、評論、轉發,也是對筆者的一種支援。
筆者精選文章
學習 sentry 原始碼整體架構,打造屬於自己的前端異常監控SDK
學習 lodash 原始碼整體架構,打造屬於自己的函數語言程式設計類庫
學習 underscore 原始碼整體架構,打造屬於自己的函數語言程式設計類庫
學習 jQuery 原始碼整體架構,打造屬於自己的 js 類庫
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:能否模擬實現JS的call和apply方法
面試官問:能否模擬實現JS的bind方法
面試官問:能否模擬實現JS的new操作符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並
關於
作者:常以若川為名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人部落格
segmentfault
前端視野專欄,開通了前端視野專欄,歡迎關注~
掘金專欄,歡迎關注~
知乎前端視野專欄,開通了前端視野專欄,歡迎關注~
github blog,求個star
^_^~
微信公眾號 若川視野
可能比較有趣的微信公眾號,長按掃碼關注。也可以加微信 lxchuan12
,註明來源,拉您進【前端視野交流群】。