- 1.理解原型設(shè)計模式以及
JavaScript中的原型規(guī)則 - 2.
instanceof的底層實現(xiàn)原理,手動實現(xiàn)一個instanceof - 3.實現(xiàn)繼承的幾種方式以及他們的優(yōu)缺點
- 4.至少說出一種開源項目(如
Node)中應用原型繼承的案例 - 5.可以描述
new一個對象的詳細過程,手動實現(xiàn)一個new操作符 - 6.理解
es6 class構(gòu)造以及繼承的底層實現(xiàn)原理
一. 理解原型設(shè)計模式以及javascript中的原則規(guī)則
原型設(shè)計模式,這種設(shè)計模式就是創(chuàng)建一個共享的原型,并通過拷貝這些原型創(chuàng)建新的對象。用于創(chuàng)建重復的對象,這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的不錯選擇??梢酝ㄟ^原型鏈實現(xiàn)原型設(shè)計模式。
原型設(shè)計模式主要的特性
- 所有函數(shù)(類)以及部分數(shù)據(jù)類型(number數(shù)值型、string字符串型、array數(shù)組型、function函數(shù)型)具有prototype屬性;
- 在prototype屬性上設(shè)置的屬性,所有的實例均可以共享;
- 在實例上可修改prototype屬性上設(shè)置的屬性
- 值類型修改:僅限當前實例發(fā)生變更
- 引用類型修改:
- 直接修改引用類型,只影響當前實例的值,并且在修改后,引用地址發(fā)生變化,后續(xù)對該實例上所有屬性更改只對當前實例起作用
- 修改應用類型的屬性或者項,父類就會發(fā)生更改,故會影響到所有實例的值
- 類可以直接設(shè)置靜態(tài)屬性,可以只用通過 ' 類名.屬性名 = 值 ' 來設(shè)置和訪問,但實例不可訪問;
var person = {
name: 'zhangsan',
age: 25,
sayHello: function(){
return this.name
}
}//先構(gòu)建一個類
var man = Object.create(person,{
job: {
value: 'IT'
}
});//利用Object.create(prototype, optionalDescriptorObjects)來使用現(xiàn)有的對象來提供新創(chuàng)建的對象的__proto__
console.log(man.sayHello()) // zhangsan
console.log(man.age) // 25
console.log(man.job) // IT
console.log(man.__proto__ === prototype) //true
可以看到,我們通過Object.create()創(chuàng)建對象,此時新建的對象就繼承自構(gòu)造器的原型對象,及繼承了初始的person,而且可以查看返回值的proto屬性和person內(nèi)的prototype是一樣的。我們常說一個對象的原型,實際上我們是在說這個對象的構(gòu)造器是有原型的。我們通過該方式,創(chuàng)建了一個新的對象,并且繼承了自構(gòu)造器的屬性,這就是原型設(shè)計模式。
在JavaScript中,對象可以使用原型克隆來實現(xiàn)獲取以及繼承原型對象的屬性和方法,很多情況下開發(fā)者會使用原型對象的Object.prototype,但是今天我們介紹了也可以通過Object.create()方法實現(xiàn)對我們需要的目標對象為原型的克隆操作,同時也可以通過修改構(gòu)造器的prototype指向來復制其它對象的屬性及方法
原型中的一些規(guī)則:
- 所有的引用數(shù)據(jù)類型(array數(shù)組類型,object對象類型,function函數(shù)類型)都具有自由擴展的屬性;
- 所有的引用數(shù)據(jù)類型都有一個proto屬性即隱式原型,其屬性值是一個普通對象;
- 所有的函數(shù),都具有一個prototype即顯式原型,其屬性值也是一個普通對象;
- 所有的引用數(shù)據(jù)類型,它的隱式原型(proto)都是指向其構(gòu)造函數(shù)的顯示原型(prototype),即(obj.proto === Object.prototype);
- 如果想獲取或利用某個對象的屬性或方法時,這個對象本身沒有這個屬性或防范,那么我們可以去它的proto即它指向的構(gòu)造函數(shù)的prototype上去查找;
二. instanceof的底層實現(xiàn)原理,手動實現(xiàn)一個instanceof
查看某對象的prototype屬性指向的原型對象是否在另一對象的原型鏈上,如果在就返回true,如果不在返回false
123 instanceof Number, //false
'dsfsf' instanceof String, //false
false instanceof Boolean, //false
[1, 2, 3] instanceof Array, //true
{a: 1} instanceof Object, //true
function () {} instanceof Function, //true
undefined instanceof Object, //false
null instanceof Object, //false
new Date() instanceof Date, //true
/^[a-zA-Z]{5,20}$/ instanceof RegExp, //true
new Error() instanceof Error //true
三. 實現(xiàn)繼承的幾種方式以及他們的優(yōu)缺點
首先,得有一個父類
// 定義一個動物類
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';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
特點:
- 非常純粹的繼承關(guān)系,實例是子類的實例,也是父類的實例
- 父類新增原型方法/原型屬性,子類都能訪問到
- 簡單,易于實現(xiàn)
缺點:
- 要想為子類新增屬性和方法,必須要在
new Animal()這樣的語句之后執(zhí)行,不能放到構(gòu)造器中 - 無法實現(xiàn)多繼承
- 來自原型對象的所有屬性被所有實例共享
- 創(chuàng)建子類實例時,無法向父類構(gòu)造函數(shù)傳參
- 構(gòu)造函數(shù)
核心:使用父類的構(gòu)造函數(shù)來增強子類實例,等于是復制父類的實例屬性給子類(沒用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特點:
- 解決了1中,子類實例共享父類引用屬性的問題
- 創(chuàng)建子類實例時,可以向父類傳遞參數(shù)
- 可以實現(xiàn)多繼承(call多個父類對象)
缺點:
- 實例并不是父類的實例,只是子類的實例
- 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
- 無法實現(xiàn)函數(shù)復用,每個子類都有父類實例函數(shù)的副本,影響性能
- 實例繼承
核心:為父類實例添加新特性,作為子類實例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
特點:
- 不限制調(diào)用方式,不管是new 子類()還是子類(),返回的對象具有相同的效果
缺點:
- 實例是父類的實例,不是子類的實例
- 不支持多繼承
- 拷貝繼承
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特點:
- 支持多繼承
缺點:
- 效率較低,內(nèi)存占用高(因為要拷貝父類的屬性)
- 無法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)
- 組合繼承
核心:通過調(diào)用父類構(gòu)造,繼承父類的屬性并保留傳參的優(yōu)點,然后通過將父類實例作為子類原型,實現(xiàn)函數(shù)復用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();// 組合繼承也需要修復構(gòu)造函數(shù)指向
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特點:
* 彌補了方式2的缺陷,可以繼承實例屬性/方法,也可以繼承原型屬性/方法
既是子類的實例,也是父類的實例
* 不存在引用屬性共享問題
* 可傳參
* 函數(shù)可復用
缺點:
* 調(diào)用了兩次父類構(gòu)造函數(shù),生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
- 寄生組合繼承
核心:通過寄生方式,砍掉父類的實例屬性,這樣,在調(diào)用兩次父類的構(gòu)造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 創(chuàng)建一個沒有實例方法的類
var Super = function(){};
Super.prototype = Animal.prototype;
//將實例作為子類的原型
Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true感謝 @bluedrink 提醒,該實現(xiàn)沒有修復constructor。Cat.prototype.constructor = Cat; // 需要修復下構(gòu)造函數(shù)
特點:堪稱完美
缺點:實現(xiàn)較為復雜
四. 至少說出一種開源項目(如Node)中應用原型繼承的案例
五. 可以描述new一個對象的詳細過程,手動實現(xiàn)一個new操作符
1、創(chuàng)建一個新的對象
2、把obj的proto指向fn的prototype,實現(xiàn)繼承
3、改變this的指向,執(zhí)行構(gòu)造函數(shù)、傳遞參數(shù),fn.apply(obj,) 或者 fn.call()
4、返回新的對象obj
function Dog(name) {
this.name = name
this.say = function () {
console.log('name = ' + this.name)
}
}
function Cat(name) {
this.name = name
this.say = function () {
console.log('name = ' + this.name)
}
}
function _new(fn, ...arg) {
const obj = {}; //創(chuàng)建一個新的對象
obj.__proto__ = fn.prototype; //把obj的__proto__指向fn的prototype,實現(xiàn)繼承
fn.apply(obj, arg) //改變this的指向
return Object.prototype.toString.call(obj) == '[object Object]'? obj : {} //返回新的對象obj
}
//測試1
var dog = _new(Dog,'aaa')
dog.say() //'name = aaa'
console.log(dog instanceof Dog) //true
console.log(dog instanceof Cat) //true
//測試2
var cat = _new(Cat, 'bbb');
cat.say() //'name = bbb'
六. 理解es6 class構(gòu)造以及繼承的底層實現(xiàn)原理
javascript使用的是原型式繼承,我們可以通過原型的特性實現(xiàn)類的繼承,
es6為我們提供了像面向?qū)ο罄^承一樣的語法糖。
class Parent {
constructor(a){
this.filed1 = a;
}
filed2 = 2;
func1 = function(){}
}
class Child extends Parent {
constructor(a,b) {
super(a);
this.filed3 = b;
}
filed4 = 1;
func2 = function(){}
}
下面我們借助babel來探究es6類和繼承的實現(xiàn)原理。
1. 類的實現(xiàn)
轉(zhuǎn)換前
class Parent {
constructor(a){
this.filed1 = a;
}
filed2 = 2;
func1 = function(){}
}
轉(zhuǎn)換后:
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function Parent(a) {
_classCallCheck(this, Parent);
this.filed2 = 2;
this.func1 = function () { };
this.filed1 = a;
};
可見class的底層依然是構(gòu)造函數(shù):
1.調(diào)用_classCallCheck方法判斷當前函數(shù)調(diào)用前是否有new關(guān)鍵字。
構(gòu)造函數(shù)執(zhí)行前有new關(guān)鍵字,會在構(gòu)造函數(shù)內(nèi)部創(chuàng)建一個空對象,將構(gòu)造函數(shù)的proptype指向這個空對象的proto,并將this指向這個空對象。如上,_classCallCheck中:this instanceof Parent 返回true。
若構(gòu)造函數(shù)前面沒有new則構(gòu)造函數(shù)的proptype不會不出現(xiàn)在this的原型鏈上,返回false。
2.將class內(nèi)部的變量和函數(shù)賦給this。
3.執(zhí)行constuctor內(nèi)部的邏輯。
4.return this (構(gòu)造函數(shù)默認在最后我們做了)。
2. 繼承實現(xiàn)
轉(zhuǎn)換前:
class Child extends Parent {
constructor(a,b) {
super(a);
this.filed3 = b;
}
filed4 = 1;
func2 = function(){}
}
轉(zhuǎn)換后:
我們先看Child內(nèi)部的實現(xiàn),再看內(nèi)部調(diào)用的函數(shù)是怎么實現(xiàn)的:
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(a, b) {
_classCallCheck(this, Child);
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));
_this.filed4 = 1;
_this.func2 = function () {};
_this.filed3 = b;
return _this;
}
return Child;
}(Parent);
1.調(diào)用_inherits函數(shù)繼承父類的proptype。
_inherits內(nèi)部實現(xiàn):
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;
}
(1) 校驗父構(gòu)造函數(shù)。
(2) 典型的寄生繼承:用父類構(gòu)造函數(shù)的proptype創(chuàng)建一個空對象,并將這個對象指向子類構(gòu)造函數(shù)的proptype。
(3) 將父構(gòu)造函數(shù)指向子構(gòu)造函數(shù)的proto(這步是做什么的不太明確,感覺沒什么意義。)
2.用一個閉包保存父類引用,在閉包內(nèi)部做子類構(gòu)造邏輯。
3.new檢查。
4.用當前this調(diào)用父類構(gòu)造函數(shù)
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));
這里的Child.proto || Object.getPrototypeOf(Child)實際上是父構(gòu)造函數(shù)(_inherits最后的操作),然后通過call將其調(diào)用方改為當前this,并傳遞參數(shù)。(這里感覺可以直接用參數(shù)傳過來的Parent)
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;
}
校驗this是否被初始化,super是否調(diào)用,并返回父類已經(jīng)賦值完的this。
5.將行子類class內(nèi)部的變量和函數(shù)賦給this。
6.執(zhí)行子類constuctor內(nèi)部的邏輯。
可見,es6實際上是為我們提供了一個“組合寄生繼承”的簡單寫法。
3. super
super代表父類構(gòu)造函數(shù)。
super.fun1() 等同于 Parent.fun1() 或 Parent.prototype.fun1()。
super() 等同于Parent.prototype.construtor()
當我們沒有寫子類構(gòu)造函數(shù)時:
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child() {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent);
可見默認的構(gòu)造函數(shù)中會主動調(diào)用父類構(gòu)造函數(shù),并默認把當前constructor傳遞的參數(shù)傳給了父類。
所以當我們聲明了constructor后必須主動調(diào)用super(),否則無法調(diào)用父構(gòu)造函數(shù),無法完成繼承。
典型的例子就是Reatc的Component中,我們聲明constructor后必須調(diào)用super(props),因為父類要在構(gòu)造函數(shù)中對props做一些初始化操作。
參考鏈接:https://blog.csdn.net/Kreme/java/article/details/102940455
https://blog.csdn.net/Kreme/java/article/details/102975973
https://www.cnblogs.com/humin/p/4556820.html
https://blog.csdn.net/qq_39985511/java/article/details/87692673
https://blog.csdn.net/qq_34149805/java/article/details/86105123