天天看点

所有JS原型相关知识点

所有JS原型相关知识点

前言

The last time, I have learned

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

也是给自己的查缺补漏和技术分享。

欢迎大家多多评论指点吐槽。

所有JS原型相关知识点
系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见GitHub 地址:Nealyang/personalBlog。目录和发文顺序皆为暂定

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

再回来说原型,老生常谈的问题了。但是着实 现在不少熟练工也貌似没有梳理清楚

Function

Object

prototype

__proto__

的关系,本文将从原型到继承到 es6 语法糖的实现来介绍系统性的介绍 JavaScript 继承。如果你能够回答上来以下问题,那么这位看官,基本这篇不用再花时间阅读了~

  • 为什么

    typeof

    判断

    null

    Object

    类型?
  • Function

    Object

    是什么关系?
  • new

    关键字具体做了什么?手写实现。
  • prototype

    __proto__

    是什么关系?什么情况下相等?
  • ES5 实现继承有几种方式,优缺点是啥
  • ES6 如何实现一个类
  • ES6

    extends

    关键字实现原理是什么

如果对以上问题有那么一些疑惑~那么。。。

所有JS原型相关知识点

THE LAST TIME 系列回顾

  • 【THE LAST TIME】彻底吃透 JavaScript 执行机制
  • 【THE LAST TIME】this:call、apply、bind

目录

虽文章较长,但较为基础。大家酌情阅读所需章节。

注意文末有思考题哦~~

  • 原型一把梭
    • 函数对象和普通对象
    • __proto__

    • prototype

    • constructor

  • typeof

    &&

    instanceof

    原理浅析
    • typeof

      基本用法
    • typeof

      原理浅析
    • instanceof

      基本用法
    • instanceof

      原理浅析
  • ES5 中的继承实现方式
    • new

      手写版本一
    • new

      手写版本二
    • new

      关键字
    • 类式继承
    • 构造函数继承
    • 组合式继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式继承
  • ES6 类的实现原理
    • _inherits

    • _possibleConstructorReturn

    • 基础类
    • 添加属性
    • 添加方法
    • extend

      关键字

原型一把梭

这。。。说是最基础没人反驳吧,说没有用有人反驳吧,说很多人到现在没梳理清楚没人反驳吧!OK~ 为什么文章那么多,你却还没有弄明白?

所有JS原型相关知识点

在概念梳理之前,我们还是放一张老掉牙所谓的经典神图:

所有JS原型相关知识点
  • function Foo

    就是一个方法,比如JavaScript 中内置的

    Array

    String

  • function Object

    就是一个

    Object

  • function Function

    就是

    Function

  • 以上都是

    function

    ,所以

    __proto__

    都是

    Function.prototype

  • 再次强调,

    String、Array、Number、Function、Object

    都是

    function

老铁,如果对这张图已非常清晰,那么可直接跳过此章节

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

函数对象和普通对象

老话说,万物皆对象。而我们都知道在 JavaScript 中,创建对象有好几种方式,比如对象字面量,或者直接通过构造函数

new

一个对象出来:

所有JS原型相关知识点

暂且我们先不管上面的代码有什么意义。至少,我们能看出,都是对象,却存在着差异性

其实在 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

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

上述代码中,

obj1

obj2

obj3

obj4

都是普通对象,

fun1

fun2

fun3

都是

Function

的实例,也就是函数对象。

所以可以看出,所有 Function 的实例都是函数对象,其他的均为普通对象,其中包括 Function 实例的实例。

所有JS原型相关知识点

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

上图中,你疑惑的点是不是

Function

new Function

的关系。其实是这样子的:

Function.__proto__ === Function.prototype//true

__proto__

首先我们需要明确两点:1️⃣

__proto__

constructor

是对象独有的。2️⃣

prototype

属性是函数独有的;

但是在 JavaScript 中,函数也是对象,所以函数也拥有

__proto__

constructor

属性。

所有JS原型相关知识点

结合上面我们介绍的

Object

Function

的关系,看一下代码和关系图

function Person(){…}; let nealyang = new Person();

所有JS原型相关知识点

proto

再梳理上图关系之前,我们再来讲解下

__proto__

所有JS原型相关知识点

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

所有JS原型相关知识点

关于更多

__proto__

更深入的介绍,可以参看工业聚大佬的《深入理解 JavaScript 原型》一文。

这里我们需要知道的是,

__proto__

是对象所独有的,并且

__proto__

是一个对象指向另一个对象,也就是他的原型对象。我们也可以理解为父类对象。它的作用就是当你在访问一个对象属性的时候,如果该对象内部不存在这个属性,那么就回去它的

__proto__

属性所指向的对象(父类对象)上查找,如果父类对象依旧不存在这个属性,那么就回去其父类的

__proto__

属性所指向的父类的父类上去查找。以此类推,知道找到

null

。而这个查找的过程,也就构成了我们常说的原型链。

prototype

object that provides shared properties for other objects

在规范里,prototype 被定义为:给其它对象提供共享属性的对象。

prototype

自己也是对象,只是被用以承担某个职能罢了.

所有对象,都可以作为另一个对象的

prototype

来用。

所有JS原型相关知识点

修改

__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

所有JS原型相关知识点

其中

constructor

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

__proto__

介绍的原型链,我们在图中直观的标出来的话就是如下这个样子
所有JS原型相关知识点

typeof && instanceof 原理

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

typeof

MDN 文档点击这里:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof

基本用法

typeof

的用法相比大家都比较熟悉,一般被用于来判断一个变量的类型。我们可以使用

typeof

来判断

number

undefined

symbol

string

function

boolean

object

这七种数据类型。但是遗憾的是,

typeof

在判断

object

类型时候,有些许的尴尬。它并不能明确的告诉你,该

object

属于哪一种

object

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

原理浅析

要想弄明白为什么

typeof

判断

null

object

,其实需要从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:对象

但是,对于

undefined

null

来说,这两个值的信息存储是有点特殊的:

  • null

    :所有机器码均为0
  • undefined

    :用 −2^30 整数来表示

所以在用

typeof

来判断变量类型的时候,我们需要注意,最好是用

typeof

来判断基本数据类型(包括

symbol

),避免对

null

的判断。

typeof

只是咱在讨论原型带出的

instanceof

的附加讨论区

instanceof

object instanceof constructor

instanceof

typeof

非常的类似。

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

为什么

Object

Function

instanceof

自己等于

true

,而其他类

instanceof

自己却又不等于

true

呢?如何解释?

要想从根本上了解

instanceof

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

原理浅析

经过上述的分析,想必大家对这种经典神图已经不那么陌生了吧,那咱就对着这张图来聊聊

instanceof

所有JS原型相关知识点

这里,我直接将规范定义翻译为 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__; } }

所以如上原理,加上上文解释的原型相关知识,我们再来解析下为什么

Object

Function

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 中的继承实现方式

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

所有JS原型相关知识点

但是本文还是希望能够基于“通俗”的方式来讲解几种常见的继承方式和优缺点。大家可多多对比查看,其实原理都是一样,名词也只是所谓的代称而已。

关于继承的文章,很多书本和博客中都有很详细的讲解。以下几种继承方式,均总结与《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__

隐式挂载。

所有JS原型相关知识点

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

如果这么写就返回

true

console.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();

所有JS原型相关知识点

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

ES6 类的实现原理

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

基础类

所有JS原型相关知识点
我们就会按照这个类,来回摩擦。然后再来分析编译后的代码。

"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

了.

添加属性

所有JS原型相关知识点

"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

也就是来判断下是否属性名重复而已。

添加方法

所有JS原型相关知识点

"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 = 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

(感觉是废话,不过的确如此)。默认

enumerable

false

configurable

true

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

extend 关键字

所有JS原型相关知识点

"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 = 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 中原型链示意图可以参照如下示意图:

所有JS原型相关知识点

图片来自冴羽的博客

关于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

,打印出来结果如下:

所有JS原型相关知识点

这里可能对

Parent.call(this,name)

有些疑惑,没关系,我们可以在 Chrome 下调试下。

所有JS原型相关知识点
所有JS原型相关知识点

可以看到,当我们

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

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

参考文献

  • 深入理解 JavaScript 原型
  • 帮你彻底搞懂JS中的prototype、__proto__与constructor
  • JavaScript instanceof 运算符深入剖析
  • JavaScript深入之创建对象的多种方式以及优缺点
  • ES6 系列之 Babel 是如何编译 Class 的(上)
  • ES6—类的实现原理
  • es6类和继承的实现原理
  • JavaScript深入之new的模拟实现

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  • 点个【在看】,或者分享转发,让更多的人也能看到这篇内容
  • 关注公众号【全栈前端精选】,不定期分享原创&精品技术文章。
  • 公众号内回复:【 1 】。加入全栈前端精选公众号交流群。
所有JS原型相关知识点

欢迎评论区留下你的精彩评论~

觉得文章不错可以分享到朋友圈让更多的小伙伴看到哦~

客官!在看一下呗

继续阅读