【THE LAST TIME】一文吃透全部JS原型相关知识点

前言

The last time, I have learnedcss

【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。html

也是给本身的查缺补漏和技术分享。前端

欢迎你们多多评论指点吐槽。node

系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见GitHub 地址:Nealyang/personalBlog。目录和发文顺序皆为暂定react

首先我想说,【THE LAST TIME】系列的的内容,向来都是包括但不限于标题的范围。git

再回来讲原型,老生常谈的问题了。可是着实 如今很多熟练工也貌似没有梳理清楚 FunctionObjectprototype__proto__的关系,本文将从原型到继承到 es6 语法糖的实现来介绍系统性的介绍 JavaScript 继承。若是你可以回答上来如下问题,那么这位看官,基本这篇不用再花时间阅读了~es6

  • 为何 typeof 判断 nullObject 类型?
  • FunctionObject 是什么关系?
  • new 关键字具体作了什么?手写实现。
  • prototype__proto__是什么关系?什么状况下相等?
  • ES5 实现继承有几种方式,优缺点是啥
  • ES6 如何实现一个类
  • ES6 extends 关键字实现原理是什么

若是对以上问题有那么一些疑惑~那么。。。github

THE LAST TIME 系列回顾

目录

虽文章较长,但较为基础。你们酌情阅读所需章节。web

注意文末有思考题哦~~面试

  • 原型一把梭
    • 函数对象和普通对象
    • __proto__
    • prototype
    • constructor
  • typeof && instanceof 原理浅析
    • typeof 基本用法
    • typeof 原理浅析
    • instanceof 基本用法
    • instanceof 原理浅析
  • ES5 中的继承实现方式
    • new 关键字
      • new 手写版本一
      • new 手写版本二
    • 类式继承
    • 构造函数继承
    • 组合式继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式继承
  • ES6 类的实现原理
    • 基础类
    • 添加属性
    • 添加方法
    • extend 关键字
      • _inherits
      • _possibleConstructorReturn

原型一把梭

这。。。说是最基础没人反驳吧,说没有用有人反驳吧,说不少人到如今没梳理清楚没人反驳吧!OK~ 为何文章那么多,你却尚未弄明白?

在概念梳理以前,咱们仍是放一张老掉牙所谓的经典神图:

  • function Foo 就是一个方法,好比JavaScript 中内置的 Array、String 等
  • function Object 就是一个 Object
  • function Function 就是 Function
  • 以上都是 function,因此 .__proto__都是Function.prototype
  • 再次强调,String、Array、Number、Function、Object都是 function

老铁,若是对这张图已很是清晰,那么可直接跳过此章节

老规矩,咱们直接来梳理概念。

函数对象和普通对象

老话说,万物皆对象。而咱们都知道在 JavaScript 中,建立对象有好几种方式,好比对象字面量,或者直接经过构造函数 new 一个对象出来:

暂且咱们先无论上面的代码有什么意义。至少,咱们能看出,都是对象,却存在着差别性

其实在 JavaScript 中,咱们将对象分为函数对象和普通对象。所谓的函数对象,其实就是 JavaScript 的用函数来模拟的类实现。JavaScript 中的 Object 和 Function 就是典型的函数对象。

关于函数对象和普通对象,最直观的感觉就是。。。咱直接看代码:

function fun1(){};
const fun2 = function(){};
const fun3 = new Function('name','console.log(name)');

const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();


console.log(typeof Object);//function
console.log(typeof Function);//function
console.log(typeof fun1);//function
console.log(typeof fun2);//function
console.log(typeof fun3);//function
console.log(typeof obj1);//object
console.log(typeof obj2);//object
console.log(typeof obj3);//object
console.log(typeof obj4);//object
复制代码

不知道你们看到上述代码有没有一些疑惑的地方~别着急,咱们一点一点梳理。

上述代码中,obj1obj2obj3obj4都是普通对象,fun1fun2fun3 都是 Function 的实例,也就是函数对象。

因此能够看出,全部 Function 的实例都是函数对象,其余的均为普通对象,其中包括 Function 实例的实例

JavaScript 中万物皆对象,而对象皆出自构造(构造函数)

上图中,你疑惑的点是否是 Functionnew Function 的关系。实际上是这样子的:

Function.__proto__ === Function.prototype//true
复制代码

__proto__

首先咱们须要明确两点:1️⃣__proto__constructor对象独有的。2️⃣prototype属性是函数独有的;

可是在 JavaScript 中,函数也是对象,因此函数也拥有__proto__constructor属性。

结合上面咱们介绍的 ObjectFunction 的关系,看一下代码和关系图

function Person(){…};
 let nealyang = new Person(); 
复制代码

__proto__

再梳理上图关系以前,咱们再来说解下__proto__

__proto__ 的例子,提及来比较复杂,能够说是一个历史问题。

ECMAScript 规范描述 prototype 是一个隐式引用,但以前的一些浏览器,已经私自实现了 __proto__这个属性,使得能够经过 obj.__proto__ 这个显式的属性访问,访问到被定义为隐式属性的 prototype

所以,状况是这样的,ECMAScript 规范说 prototype 应当是一个隐式引用:

  • 经过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象
  • 经过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象
  • 部分浏览器提早开了 __proto__ 的口子,使得能够经过 obj.__proto__ 直接访问原型,经过 obj.__proto__ = anotherObj 直接设置原型
  • ECMAScript 2015 规范只好向事实低头,将 __proto__ 属性归入了规范的一部分

从浏览器的打印结果咱们能够看出,上图对象 a 存在一个__proto__属性。而事实上,他只是开发者工具方便开发者查看原型的故意渲染出来的一个虚拟节点。虽然咱们能够查看,但实则并不存在该对象上。

__proto__属性既不能被 for in 遍历出来,也不能被 Object.keys(obj) 查找出来。

访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象上 __proto__ 属性的 get/set 方法。

Object.defineProperty(Object.prototype,'__proto__',{
	get(){
		console.log('get')
	}
});

({}).__proto__;
console.log((new Object()).__proto__);
复制代码

关于更多__proto__更深刻的介绍,能够参看工业聚大佬的《深刻理解 JavaScript 原型》一文。

这里咱们须要知道的是,__proto__是对象所独有的,而且__proto__一个对象指向另外一个对象,也就是他的原型对象。咱们也能够理解为父类对象。它的做用就是当你在访问一个对象属性的时候,若是该对象内部不存在这个属性,那么就回去它的__proto__属性所指向的对象(父类对象)上查找,若是父类对象依旧不存在这个属性,那么就回去其父类的__proto__属性所指向的父类的父类上去查找。以此类推,知道找到 null。而这个查找的过程,也就构成了咱们常说的原型链

prototype

object that provides shared properties for other objects

在规范里,prototype 被定义为:给其它对象提供共享属性的对象。prototype 本身也是对象,只是被用以承担某个职能罢了.

全部对象,均可以做为另外一个对象的 prototype 来用。

修改__proto__的关系图,咱们添加了 prototype,prototype是函数所独有的。**它的做用就是包含能够给特定类型的全部实例提供共享的属性和方法。它的含义就是函数的远行对象,**也就是这个函数所建立的实例的远行对象,正如上图:nealyang.__proto__ === Person.prototype。 任何函数在建立的时候,都会默认给该函数添加 prototype 属性.

constructor

constructor属性也是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数

注意,每个对象都有其对应的构造函数,自己或者继承而来。单从constructor这个属性来说,只有prototype对象才有。每一个函数在建立的时候,JavaScript 会同时建立一个该函数对应的prototype对象,而函数建立的对象.__proto__ === 该函数.prototype,该函数.prototype.constructor===该函数自己,故经过函数建立的对象即便本身没有constructor属性,它也能经过__proto__找到对应的constructor,因此任何对象最终均可以找到其对应的构造函数。

惟一特殊的可能就是我开篇抛出来的一个问题。JavaScript 原型的老祖宗:Function。它是它本身的构造函数。因此Function.prototype === Function.__proto

为了直观了解,咱们在上面的图中,继续添加上constructor

其中 constructor 属性,虚线表示继承而来的 constructor 属性

__proto__介绍的原型链,咱们在图中直观的标出来的话就是以下这个样子

typeof && instanceof 原理

问什么好端端的说原型、说继承会扯到类型判断的原理上来呢。毕竟原理上有一丝的联系,每每面试也是由浅入深、顺藤摸瓜的拧出整个知识面。因此这里咱们也简单说一下吧。

typeof

MDN 文档点击这里:developer.mozilla.org/zh-CN/docs/…

基本用法

typeof 的用法想必你们都比较熟悉,通常被用于来判断一个变量的类型。咱们可使用 typeof 来判断numberundefinedsymbolstringfunctionbooleanobject 这七种数据类型。可是遗憾的是,typeof 在判断 object 类型时候,有些许的尴尬。它并不能明确的告诉你,该 object 属于哪种 object

let s = new String('abc');
typeof s === 'object'// true
typeof null;//"object"
复制代码

原理浅析

要想弄明白为何 typeof 判断 nullobject,其实须要从js 底层如何存储变量类型来讲其。虽说,这是 JavaScript 设计的一个 bug。

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。因为 null 表明的是空指针(大多数平台下值为 0x00),所以,null 的类型标签是 0,typeof null 也所以返回 "object"。曾有一个 ECMAScript 的修复提案(经过选择性加入的方式),但被拒绝了。该提案会致使 typeof null === 'null'

js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息:

  • 1:整数
  • 110:布尔
  • 100:字符串
  • 010:浮点数
  • 000:对象

可是,对于 undefinednull 来讲,这两个值的信息存储是有点特殊的:

  • null:全部机器码均为0
  • undefined:用 −2^30 整数来表示

因此在用 typeof 来判断变量类型的时候,咱们须要注意,最好是用 typeof 来判断基本数据类型(包括symbol),避免对 null 的判断。

typeof 只是咱在讨论原型带出的 instanceof 的附加讨论区

instanceof

object instanceof constructor

instanceoftypeof 很是的相似。instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。与 typeof 方法不一样的是,instanceof 方法要求开发者明确地确认对象为某特定类型。

基本用法

// 定义构造函数
function C(){} 
function D(){} 

var o = new C();


o instanceof C; // true,由于 Object.getPrototypeOf(o) === C.prototype


o instanceof D; // false,由于 D.prototype 不在 o 的原型链上

o instanceof Object; // true,由于 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 由于 C.prototype 如今在 o3 的原型链上
复制代码

如上,是 instanceof 的基本用法,它能够判断一个实例是不是其父类型或者祖先类型的实例。

console.log(Object instanceof Object);//true 
console.log(Function instanceof Function);//true 
console.log(Number instanceof Number);//false 
console.log(String instanceof String);//false 

console.log(Function instanceof Object);//true 

console.log(Foo instanceof Function);//true 
console.log(Foo instanceof Foo);//false
复制代码

为何 ObjectFunction instanceof 本身等于 true,而其余类 instanceof 本身却又不等于 true 呢?如何解释?

要想从根本上了解 instanceof 的奥秘,须要从两个方面着手:1,语言规范中是如何定义这个运算符的。2,JavaScript 原型继承机制。

原理浅析

通过上述的分析,相比你们对这种经典神图已经不那么陌生了吧,那咱就对着这张图来聊聊 instanceof

这里,我直接将规范定义翻译为 JavaScript 代码以下:

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
 var O = R.prototype;// 取 R 的显示原型
 L = L.__proto__;// 取 L 的隐式原型
 while (true) { 
   if (L === null) 
     return false; 
   if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true 
     return true; 
   L = L.__proto__; 
 } 
}
复制代码

因此如上原理,加上上文解释的原型相关知识,咱们再来解析下为何ObjectFunction instanceof 本身等于 true

  • Object instanceof Object
// 为了方便表述,首先区分左侧表达式和右侧表达式
ObjectL = Object, ObjectR = Object; 
// 下面根据规范逐步推演
O = ObjectR.prototype = Object.prototype 
L = ObjectL.__proto__ = Function.prototype 
// 第一次判断
O != L 
// 循环查找 L 是否还有 __proto__ 
L = Function.prototype.__proto__ = Object.prototype 
// 第二次判断
O == L 
// 返回 true
复制代码
  • Function instanceof Function
// 为了方便表述,首先区分左侧表达式和右侧表达式
FunctionL = Function, FunctionR = Function; 
// 下面根据规范逐步推演
O = FunctionR.prototype = Function.prototype 
L = FunctionL.__proto__ = Function.prototype 
// 第一次判断
O == L 
// 返回 true
复制代码
  • Foo instanceof Foo
// 为了方便表述,首先区分左侧表达式和右侧表达式
FooL = Foo, FooR = Foo; 
// 下面根据规范逐步推演
O = FooR.prototype = Foo.prototype 
L = FooL.__proto__ = Function.prototype 
// 第一次判断
O != L 
// 循环再次查找 L 是否还有 __proto__ 
L = Function.prototype.__proto__ = Object.prototype 
// 第二次判断
O != L 
// 再次循环查找 L 是否还有 __proto__ 
L = Object.prototype.__proto__ = null 
// 第三次判断
L == null 
// 返回 false
复制代码

ES5 中的继承实现方式

在继承实现上,工业聚大大在他的原型文章中,将原型继承分为两大类,显式继承和隐式继承。感兴趣的能够点击文末参考连接查看。

可是本文仍是但愿可以基于“通俗”的方式来说解几种常见的继承方式和优缺点。你们可多多对比查看,其实原理都是同样,名词也只是所谓的代称而已。

关于继承的文章,不少书本和博客中都有很详细的讲解。如下几种继承方式,均总结与《JavaScript 设计模式》一书。也是笔者三年前写的一篇文章了。

new 关键字

在讲解继承以前呢,我以为 new 这个东西颇有必要介绍下~

一个例子看下new 关键字都干了啥

function Person(name,age){
  this.name = name;
  this.age = age;
  
  this.sex = 'male';
}

Person.prototype.isHandsome = true;

Person.prototype.sayName = function(){
  console.log(`Hello , my name is ${this.name}`);
}

let handsomeBoy = new Person('Nealyang',25);

console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true

handsomeBoy.sayName(); // Hello , my name is Nealyang
复制代码

从上面的例子咱们能够看到:

  • 访问到 Person 构造函数里的属性
  • 访问到 Person.prototype 中的属性

new 手写版本一

function objectFactory() {

    const obj = new Object(),//从Object.prototype上克隆一个对象

    Constructor = [].shift.call(arguments);//取得外部传入的构造器

    const F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正确的原型

    Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性

    return obj;//返回 obj

};
复制代码
  • new Object() 的方式新建了一个对象 obj
  • 取出第一个参数,就是咱们要传入的构造函数。此外由于 shift 会修改原数组,因此 arguments 会被去除第一个参数
  • 将 obj 的原型指向构造函数,这样 obj 就能够访问到构造函数原型中的属性
  • 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就能够访问到构造函数中的属性
  • 返回 obj

下面咱们来测试一下:

function Person(name,age){
  this.name = name;
  this.age = age;
  
  this.sex = 'male';
}

Person.prototype.isHandsome = true;

Person.prototype.sayName = function(){
  console.log(`Hello , my name is ${this.name}`);
}

function objectFactory() {

    let obj = new Object(),//从Object.prototype上克隆一个对象

    Constructor = [].shift.call(arguments);//取得外部传入的构造器
    
    console.log({Constructor})

    const F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正确的原型

    Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性

    return obj;//返回 obj

};

let handsomeBoy = objectFactory(Person,'Nealyang',25);

console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true

handsomeBoy.sayName(); // Hello , my name is Nealyang
复制代码

注意上面咱们没有直接修改 obj 的__proto__隐式挂载。

new 手写版本二

考虑构造函数又返回值的状况:

  • 若是构造函数返回一个对象,那么咱们也返回这个对象
  • 如上不然,就返回默认值
function objectFactory() {

    var obj = new Object(),//从Object.prototype上克隆一个对象

    Constructor = [].shift.call(arguments);//取得外部传入的构造器

    var F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正确的原型

    var ret = Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性

    return typeof ret === 'object' ? ret : obj;//确保构造器老是返回一个对象

};
复制代码

关于 call、apply、bind、this 等用法和原理讲解:【THE LAST TIME】this:call、apply、bind

类式继承

function SuperClass() {
  this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
  return this.superValue;
}

function SubClass() {
  this.subValue = false;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getSubValue = function() {
  return this.subValue;
}

var instance = new SubClass();

console.log(instance instanceof SuperClass)//true
console.log(instance instanceof SubClass)//true
console.log(SubClass instanceof SuperClass)//false
复制代码

从咱们以前介绍的 instanceof 的原理咱们知道,第三个 console 若是这么写就返回 trueconsole.log(SubClass.prototype instanceof SuperClass)

虽然实现起来清晰简洁,可是这种继承方式有两个缺点:

  • 因为子类经过其原型prototype对父类实例化,继承了父类,因此说父类中若是共有属性是引用类型,就会在子类中被全部的实例所共享,所以一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其余的子类
  • 因为子类实现的继承是靠其原型prototype对父类进行实例化实现的,所以在建立父类的时候,是没法向父类传递参数的。于是在实例化父类的时候也没法对父类构造函数内的属性进行初始化

构造函数继承

function SuperClass(id) {
  this.books = ['js','css'];
  this.id = id;
}
SuperClass.prototype.showBooks = function() {
  console.log(this.books);
}
function SubClass(id) {
  //继承父类
  SuperClass.call(this,id);
}
//建立第一个子类实例
var instance1 = new SubClass(10);
//建立第二个子类实例
var instance2 = new SubClass(11);

instance1.books.push('html');
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError
复制代码

SuperClass.call(this,id)固然就是构造函数继承的核心语句了.因为父类中给this绑定属性,所以子类天然也就继承父类的共有属性。因为这种类型的继承没有涉及到原型prototype,因此父类的原型方法天然不会被子类继承,而若是想被子类继承,就必须放到构造函数中,这样建立出来的每个实例都会单独的拥有一份而不能共用,这样就违背了代码复用的原则,因此综合上述两种,咱们提出了组合式继承方法

组合式继承

function SuperClass(name) {
  this.name = name; 
  this.books = ['Js','CSS'];
}
SuperClass.prototype.getBooks = function() {
    console.log(this.books);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getTime = function() {
  console.log(this.time);
}
复制代码

如上,咱们就解决了以前说到的一些问题,可是是否是从代码看,仍是有些不爽呢?至少这个SuperClass的构造函数执行了两遍就感受很是的不妥.

原型式继承

function inheritObject(o) {
    //声明一个过渡对象
  function F() { }
  //过渡对象的原型继承父对象
  F.prototype = o;
  //返回过渡对象的实例,该对象的原型继承了父对象
  return new F();
}
复制代码

原型式继承大体的实现方式如上,是否是想到了咱们new关键字模拟的实现?

其实这种方式和类式继承很是的类似,他只是对类式继承的一个封装,其中的过渡对象就至关于类式继承的子类,只不过在原型继承中做为一个普通的过渡对象存在,目的是为了建立要返回的新的实例对象。

var book = {
    name:'js book',
    likeBook:['css Book','html book']
}
var newBook = inheritObject(book);
newBook.name = 'ajax book';
newBook.likeBook.push('react book');
var otherBook = inheritObject(book);
otherBook.name = 'canvas book';
otherBook.likeBook.push('node book');
console.log(newBook,otherBook);
复制代码

如上代码咱们能够看出,原型式继承和类式继承一个样子,对于引用类型的变量,仍是存在子类实例共享的状况。

因此,咱们还有下面的寄生式继

寄生式继承

var book = {
    name:'js book',
    likeBook:['html book','css book']
}
function createBook(obj) {
    //经过原型方式建立新的对象
  var o = new inheritObject(obj);
  // 拓展新对象
  o.getName = function(name) {
    console.log(name)
  }
  // 返回拓展后的新对象
  return o;
}
复制代码

其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新建立的对象不只仅有父类的属性和方法,还新增了别的属性和方法。

寄生组合式继承

回到以前的组合式继承,那时候咱们将类式继承和构造函数继承组合使用,可是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,因此才有了寄生组合式继承

而寄生组合式继承是寄生式继承和构造函数继承的组合。可是这里寄生式继承有些特殊,这里他处理不是对象,而是类的原型。

function inheritObject(o) {
  //声明一个过渡对象
  function F() { }
  //过渡对象的原型继承父对象
  F.prototype = o;
  //返回过渡对象的实例,该对象的原型继承了父对象
  return new F();
}

function inheritPrototype(subClass,superClass) {
    // 复制一份父类的原型副本到变量中
  var p = inheritObject(superClass.prototype);
  // 修正由于重写子类的原型致使子类的constructor属性被修改
  p.constructor = subClass;
  // 设置子类原型
  subClass.prototype = p;
}
复制代码

组合式继承中,经过构造函数继承的属性和方法都是没有问题的,因此这里咱们主要探究经过寄生式继承从新继承父类的原型。

咱们须要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,咱们已经调用了父类的构造函数。所以咱们须要的就是父类的原型对象的一个副本,而这个副本咱们能够经过原型继承拿到,可是这么直接赋值给子类会有问题,由于对父类原型对象复制获得的复制对象p中的constructor属性指向的不是subClass子类对象,所以在寄生式继承中要对复制对象p作一次加强,修复起constructor属性指向性不正确的问题,最后将获得的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型而且没有执行父类的构造函数。

function SuperClass(name) {
  this.name = name;
  this.books=['js book','css book'];
}
SuperClass.prototype.getName = function() {
  console.log(this.name);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
inheritPrototype(SubClass,SuperClass);
SubClass.prototype.getTime = function() {
  console.log(this.time);
}
var instance1 = new SubClass('React','2017/11/11')
var instance2 = new SubClass('Js','2018/22/33');

instance1.books.push('test book');

console.log(instance1.books,instance2.books);
instance2.getName();
instance2.getTime();
复制代码

这种方式继承其实如上图所示,其中最大的改变就是子类原型中的处理,被赋予父类原型中的一个引用,这是一个对象,所以有一点你须要注意,就是子类在想添加原型方法必须经过prototype.来添加,不然直接赋予对象就会覆盖从父类原型继承的对象了.

ES6 类的实现原理

关于 ES6 中的 class 的一些基本用法和介绍,限于篇幅,本文就不作介绍了。该章节,咱们主要经过 babel的 REPL来查看分析 es6 中各个语法糖包括继承的一些实现方式。

基础类

咱们就会按照这个类,来回摩擦。而后再来分析编译后的代码。

"use strict";

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");
  }
}

var Person = function Person(name) {
  _classCallCheck(this, Person);

  this.name = name;
};
复制代码

_instanceof就是来判断实例关系的的。上述代码就比较简单了,_classCallCheck的做用就是检查 Person 这个类,是不是经过new 关键字调用的。毕竟被编译成 ES5 之后,function 能够直接调用,可是若是直接调用的话,this 就指向 window 对象,就会Throw Error了.

添加属性

"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

function _defineProperty(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

var Person = function Person(name) {
    _classCallCheck(this, Person);

    _defineProperty(this, "shili", '实例属性');

    this.name = name;
};

_defineProperty(Person, "jingtai", ' 静态属性');
复制代码

其实就是讲属性赋值给谁的问题。若是是实例属性,直接赋值到 this 上,若是是静态属性,则赋值类上。_defineProperty也就是来判断下是否属性名重复而已。

添加方法

"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

function _defineProperty(obj, key, value) {...}

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);
    }
}

function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

var Person =
    /*#__PURE__*/
    function () {
        function Person(name) {
            _classCallCheck(this, Person);

            _defineProperty(this, "shili", '实例属性');

            this.name = name;
        }

        _createClass(Person, [{
            key: "sayName",
            value: function sayName() {
                return this.name;
            }
        }, {
            key: "name",
            get: function get() {
                return 'Nealyang';
            },
            set: function set(newName) {
                console.log('new name is :' + newName);
            }
        }], [{
            key: "eat",
            value: function eat() {
                return 'eat food';
            }
        }]);

        return Person;
    }();

_defineProperty(Person, "jingtai", ' 静态属性');
复制代码

看起来代码量还很多,其实就是一个_createClass函数和_defineProperties函数而已。

首先看_createClass这个函数的三个参数,第一个是构造函数,第二个是须要添加到原型上的函数数组,第三个是添加到类自己的函数数组。其实这个函数的做用很是的简单。就是增强一下构造函数,所谓的增强构造函数就是给构造函数或者其原型上添加一些函数。

_defineProperties就是多个_defineProperty(感受是废话,不过的确如此)。默认 enumerablefalseconfigurabletrue

其实如上就是 es6 class 的实现原理。

extend 关键字

"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

var Parent = function Parent(name) {...};

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);
}

function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}

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");
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            writable: true,
            configurable: true
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}

var Child =
    /*#__PURE__*/
    function (_Parent) {
        _inherits(Child, _Parent);

        function Child(name, age) {
            var _this;

            _classCallCheck(this, Child);

            _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name)); // 调用父类的 constructor(name)

            _this.age = age;
            return _this;
        }

        return Child;
    }(Parent);

var child1 = new Child('全栈前端精选', '0.3');
console.log(child1);
复制代码

删去类相关的代码生成,剩下的就是继承的语法糖剖析了。其中super 关键字表示父类的构造函数,至关于 ES5 的 Parent.call(this),而后再根据咱们上文说到的继承方式,有没有感受该集成的实现跟咱们说的寄生组合式继承很是的类似呢?

在 ES6 class 中,子类必须在 constructor 方法中调用 super 方法,不然新建实例时会报错。这是由于子类没有本身的 this 对象,而是继承父类的 this 对象,而后对其进行加工。若是不调用 super 方法,子类就得不到 this 对象。

也正是由于这个缘由,在子类的构造函数中,只有调用 super 以后,才可使用 this 关键字,不然会报错。

关于 ES6 中原型链示意图能够参照以下示意图:

图片来自冴羽的博客

关于ES6 中的 extend 关键字,上述代码咱们彻底能够根据执行来看。其实重点代码无非就两行:

_inherits(Child, _Parent);
  _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name)); 
复制代码

咱们分别来分析下具体的实现:

_inherits

代码比较简单,都是上文提到的内容,就是创建 Child 和 Parent 的原型链关系。代码解释已备注在代码内

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {//subClass 类型判断
        throw new TypeError("Super expression must either be null or a function");
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {//Object.create 第二个参数是给subClass.prototype添加了 constructor 属性
            value: subClass,
            writable: true,
            configurable: true//注意这里enumerable没有指名,默认是 false,也就是说constructor为不可枚举的。
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}
复制代码

_possibleConstructorReturn

_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
复制代码

根据上图咱们整理的 es6 原型图可知:

Child.prototype === Parent
复制代码

因此上面的代码咱们能够翻译为:

_this = _possibleConstructorReturn(this, Parent.call(this, name));
复制代码

而后咱们再一层一层拨源码的实现

function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}
复制代码

上述代码,self其实就是 Child 的 IIFE返回的 function new 调用的 this,打印出来结果以下:

这里可能对Parent.call(this,name)有些疑惑,不要紧,咱们能够在 Chrome 下调试下。

能够看到,当咱们 Parent 的构造函数这么写

class Parent {
    constructor(name) {
        this.name = name;
    }
}
复制代码

那么最终,传递给_possibleConstructorReturn函数的第二参数 call就是一个 undefined。因此在_possibleConstructorReturn函数里面会对 call进行判断,返回正确的 this 指向:Child

因此总体代码的目的就是根据 Parent 构造函数的返回值类型肯定子类构造函数 this 的初始值 _this

最后

【THE LAST TIME】系列关于 JavaScript 基础的文章目前更新三篇,咱们最后再来一道经典的面试题吧!

function Foo() {
  getName = function() {
    alert(1);
  };
  return this;
}
Foo.getName = function() {
  alert(2);
};
Foo.prototype.getName = function() {
  alert(3);
};
var getName = function() {
  alert(4);
};
function getName() {
  alert(5);
}

//请写出如下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
复制代码

老铁,评论区留下你的思考吧~

学习交流

关注公众号: 【全栈前端精选】 不按期获取好文推荐及原创文章。

公众号内回复 【1】,加入全栈前端学习群,一块儿交流。

参考文献