学前端:JS的面向对象,原型链

119 阅读4分钟

引言

众所周知,JS是一门面向对象的语言,提到面向对象,也许会让人联想到 继承,封装,多态等概念,但是,千万不要带着Java的思维来学JavaScript,尽管他们是哥俩。JS面向对象不同于一些后台语言,其核心机制是原型链。在探究这个机制之前我们还是来明确几个概念。

基本概念:

  • 对象,JavaScript的对象是一种无序的集合数据类型,它由若干键值对组成。
  • 类,可用来构建对象的函数,也可以叫构造函数
  • 实例,由类或构造函数创建的对象
  • 函数的原型(prototype),正常情况下,每个函数都会带有一个原型对象
  • 对象的原型([[prototype]]/_ proto _ ),指向对象所属类的原型

函数的3种角色:

  • 1.普通对象,直接调用属性(Object.keys())
  • 2.普通函数,直接执行(Object())
  • 3.构造函数(new Object())

特殊情况:

  • 没有原型的函数是不可以 new执行的
    • 没有原型的函数:1.箭头函数 2.采用ES6对象成员快捷写法的函数 3.在class内书写,当作公共属性放在类原型上的函数
  • 在class的写法下,原型上的方法通过实例调用和原型调用所输出的this是不同的
  • new A().fn() ,fn的this是A的实例,A.prototype.fn() , fn的this是构造函数A

构造函数执行过程

构造函数执行相比普通函数有一些不同:

  • 1.初始化作用域链
  • 2.创建一块堆内存空间(不同)
  • 3.初始化this,this指向2创建的那块空间
  • 4.初始化arguments
  • 5.形参赋值
  • 6.变量提升
  • 7.代码执行
    • 这时对this的所有操作都作用在开辟的堆内存上(不同)
      若代码中没有返回值或返回原始值,最终会把创建的堆内存地址返回出去(不同)

new 执行构造函数,得到的实例对象的[[prototype]]属性指向 构造函数的prototype,这个就是原型链。当访问一个实例对象内不存在的属性,JS引擎就会沿着原型链向上寻找

举个例子:
    function fn (name,age) {
        this.name = name
        this.age = age
    }
    let obj = new fn('jack',25)
    
    fn 作为构造函数 new 执行所创建的obj对象,其原型链如下:

image.png

js的多种继承

1.原型继承:

  • Child.prototype = new Parent
    子类原型上有父类公有的和私有的

2.call继承

  • Child(){ Parent.call(this) }
    子类实例包含父类私有的,但找不到父类公有的

3.寄生组合继承(call继承 + 修改原型指向 )

  • Child(){ Parent.call(this) }
    Child.prototype=Object.assign(Object.create(Parent.prototype),Child.prototype)
    父类私有的也是子类私有的,公有的也是子类公有的

4.ES6的class继承

  • class Child extends Parent {}
    子类必须把super()写在constructor内第一行
    Child.prototype.__proto__ 自动指向 Parent.prototype
    Child._ proto _ 自动指向 Parent

区分[[prototype]] 和 _ proto _

一般情况下,在同一对象内的这两者都是指向同一结果(当前对象所属类的原型)。但是,如果都一样的话为什么要存在2个形式呢?

首先,我们获取对象原型的方式有两种:

    1. Object.getPrototypeOf(XXX)
    1. XXX._ _ proto _ _ 关于_ proto _ 其实最开始是浏览器厂商先实现的,之后ECMA262规范发现好多厂商都实现了这个API,所以才加入规范里。 image.png

进一步了解_ _ proto_ _

我们可以通过_ _ proto_ _ 来访问对象的原型,似乎给了我们一种错觉,_ _ proto_ _ 是对象的属性,但其实不是的,_ _ proto_ _ 是位于Object.prototype上的访问器属性(accessor property)。只有继承了这个原型的对象才能使用_ _ proto_ _ 获取原型。 image.png

let obj1= Object.create(null)    / / 得到一个纯净的对象,没有原型也没有任何属性
let obj2= Object.create(obj1)    / / 此时obj2不能通过__proto__ 访问到它的原型obj1

原型重定向

既然我们知道了原型链的原理,那么我们就可以通过修改对象的原型链来给他扩展一些公共方法(根据MDN的文档,这样很耗费性能,不建议这么做)

举例:

  let obj = {}
  obj.__proto__ = Function.prototype
  这样我们就可以拿到 call apply bind 等方法了,但是这并不意味着obj变成了函数因为obj 不具有`[[call]]`属性 

有趣的现象:

  • 箭头函数是没有原型的,所以他不能作为构造函数 new执行,但是,原因真的这么简单吗?其实箭头函数不能new执行是因为他没有[[constructor]]属性
  • 如果我们创建了一个函数fn,然后让fn.prototype = null,那么 new fn创建的对象的[[prototype]]指向的是Object.prototype

函数的创建过程

要想再深入的探究下去,我们需要去看看官方文档对函数创建的描述,但是由于规范的更新,很多东西出现了变动,所以我截取了ES5,和ES8的规范进行对比。

ES5 的描述 image.png

ES8 的描述 image.png

参考

[1] stackoverflow.com/questions/6…

[2] stackoverflow.com/questions/4…

[3] developer.mozilla.org/zh-CN/docs/…

[4] 262.ecma-international.org/8.0/#sec-fu…

[5] 262.ecma-international.org/5.1/#sec-13