《前端竹节》(3)【原型与对象】

作前端开发有段时间了,遇到过不少坎,如果要排出个前后顺序,那么JavaScript的原型与对象绝对逃不出TOP3。前端

若是说前端是海,JavaScript就是海里的水

一直以来都想写篇文章梳理一下这块,为了加深本身的理解,也为了帮助后来者尽快出坑,但总觉缺乏恰当的切入点,使读者能看到清晰的路径而非生硬的教科书。最近看到句话“好的问题如庖丁之刃,能帮你轻松剖开现象直达本质”,因此本文以层层探问解答的方式,试图提供一个易于理解的角度。编程

如今的软件开发,不多有不是面向对象的,那么JavaScript如何建立对象?

1、 建立对象的方法

在传统的面向对象编程语言(如:C++,Java等)中,都用定义类的关键字class,首先声明一个类,而后再经过类实例化出对象实例。但在JavaScript中若实现这样逻辑的对象建立,须要先定义一个表明类的构造函数,再经过new运算符执行构造函数实例化出对象。数组

  1. 对象字面量编程语言

    var object1 = { name: "object1" }
  2. 构造函数法函数

    var ClassMethod = function() {
        this.name = "Class"
    }
    var object2 = new ClassMethod()
    // 这种方式建立的对象字面量
    var object3 = new Object({ name: "object3" })

    这里提到的new运算符,后面会详述优化

  3. Object.create(proto)
    建立一个新对象,使用入参proto对象来提供新建立的对象的__proto__,也就入参对象时新建立对象的原型对象。this

    var Parent = { name: "Parent" }
    var object4 = Object.create(Parent)
想要明白JavaScript原型继承的幺蛾子,势必要搞清楚原型对象、实例对象、构造函数以及原型链的概念和关系,接下来我尽可能作到表述地结构清晰,言简意赅。

2、原型继承

暂时搁置一下原型链,我先讲清楚其他三个概念的门门道道,若是你手边有纸笔最好,没有在脑中想象也不复杂。prototype

  1. 画一个等边三角形,从顶点顺时针为每一个角编号(1)、(2)、(3)
  2. 其中(1)旁边标注“原型对象”,(2)构造函数,(3)实例对象
  3. 从(2)构造函数(如上节例中的ClassMethod)指向(3)实例对象(上节例中的object2)画一条带箭头的线。线上注明new运算符,表示var object2 = new ClassName()
  4. 从(2)构造函数指向(1)原型对象画一条带箭头的线。线上标注prototype,表示该构造函数的原型对象等于ClassName.prototype。(函数都有prototype属性,指向它的原型对象)
  5. 从(3)实例对象指向(1)原型对象画一条带箭头的线。线上标注__proto__,表示该实例对象的原型对象等于object2.__proto__,结合第4步,便有ClassName.prototype === object2.__proto__
  6. 从(1)原型对象指向(2)构造函数画一条带箭头的线。线上标注constructor,表示该原型对象的构造函数等于ClassName === object2.__proto__.constructor

关于JavaScript函数与对象自带的属性有一句须要画重点的话:全部的对象都有一个__proto__属性指向其原型对象,全部的函数都有prototype属性,指向它的原型对象。函数其实也是一种对象,那么函数便有两个原型对象。因为平时更关注对象依据__proto__属性,指向的原型对象所构成的原型链,为了区分函数的两个原型,便将__proto__所指的原型对象称做隐式原型,而把prototype所指向的原型对象称做显示原型指针

看到这里你应该已经知道原型对象、实例对象、构造函数以及原型链是什么了,可是对于为何是这样应该还比较懵,由于我也曾如此,用以往类与对象,父类与子类的概念对照原型与实例,试图想找出一些熟悉的关系,让本身可以理解。code

人们老是习惯经过熟悉的事物,类比去认识陌生的事物。这或许是一种快速的方式,但这绝对不是一种有效的方式。类比总会让咱们轻视逻辑推理

3、从instanceof再看原型链

语法格式为object instanceof constructor,从字面上理解instanceof,是用来判断object是否为constructor构造函数实例化出的对象。但除此以外,若构造函数所指的显示原型对象constructor.prototype存在于object的原型链上,结果也都会为true

字面理解多少会有些误差,请及时 查阅MDN文档

原型链就是JavaScript相关对象之间,由__proto__属性依次引用造成的有向关系链,原型对象上的属性和方法能够被其实例对象使用。(这种有向的父子关系链就具备了实现类继承的特性)

4、new运算符

new Foo()执行过程当中,都发生了什么?

如下三步:

  1. 建立一个继承自Foo.prototype的新对象。
  2. 执行构造函数Foo,并将this指针绑定到新建立的对象上。
  3. 若是构造函数返回一个对象,则这个对象就是new运算符执行的结果;若是没返回对象,则使用第一步建立出的新对象。

为了直观的理解,这里自定义一个函数myNew来模拟new运算符

function myNew(Foo){
    var tmp = Object.create(Foo.prototype)
    var ret = Foo.call(tmp)
    if (typeof ret === 'object') {
        return ret
    } else {
        return tmp
    }
}

5、实现继承

在ES6中,出现了更为直观的语法糖形式: class Child extends Parent{},但这里咱们只看看以前没有这种语法糖是怎么实现的。我一直有一个体会: 要想快速的了解一个事物,就去了解它的源起流变

首先定义一个父类Parent,以及它的一个属性name:

function Parent() {
    this.name = 'parent'
}

接下来如何定义一个继承自Parent的子类Child

  1. 构造函数方式

    function Child() {
        Parent.call(this)
        this.type = 'subClass' // ... 这里还可定义些子类的属性和方法
    }

    这种方式的缺陷是:父类原型链上的属性和方法不会被子类继承。

  2. 原型链方式

    function Child() {
        this.type = 'subClass'
    }
    Child.prototype = new Parent()

    这种方式弥补了子类无法继承父类原型链上属性和方法的缺陷,与此同时又引入一个新的问题:父类上的对象或数组属性会引用传递给子类实例。
    好比父类上有一个数组属性arr,现经过new Child()实例化出两个实例对象c1c2,那么c1对其arr属性的操做同时也会引发c2.arr的改变,这固然不是咱们想要的。

  3. 组合方式(综合1,2两种方式)

    function Child() {
        Parent.call(this)
        this.type = 'subClass'
    }
    Child.prototype = new Parent()

    虽然解决了上述问题,但明显看到这里构造函数执行了两遍,显然有些多余。

  4. 组合优化方式

    function Child() {
        Parent.call(this)
        this.type = 'subClass'
    }
    Child.prototype = Parent.prototype

    这种方式减小了多余的父类构造函数调用,但子类的显示原型会被覆盖。此例中经过子类构造函数实例化一个对象:var cObj = new Child(),能够验证出实例对象的原型对象,是父类构造函数的显示原型:cObj.__proto__.constructor === Parent,显然这种方式依旧不很完美。

  5. 终极方式

    function Child() {
        Parent.call(this)
        this.type = 'subClass'
    }
    Child.prototype = Object.create(Parent.prototype)
    Child.prototype.constructor = Child

    实例对象的__proto__属性值老是该实例对象的构造函数的prototype属性。这里关于构造函数的从属关系存在一个易混淆的点,我多啰嗦几句来试图把这块讲清楚:还记的上面咱们画的那个三角形么?三个角分别表明构造函数、实例对象和原型对象,三条有向边分别表明new,__proto__,prototype,根据__proto__有向边串联起来链即是原型链。

    要解释清楚构造函数的从属关系,咱们先在上面所画的原型链三角形中的每一个三角形中,添加一条有向边:从原型对象指向构造函数,这表示原型对象有一个 constructor属性指向它的构造函数,而该构造函数的 prototype属性又指向这个构造函数,因而便在局部造成了一个有向环。

    如今一切都协调了,惟独还有一点,就是原型链末端的实例对象构造函数的指向,不论经过new运算符仍是经过Object.create建立出来的实例对象的constructor属性,都和其原型对象的constructor相同。因此为了保持一致性便有了上面那句Child.prototype.constructor = Child,为的是在你想要知道一个对象是由哪一个构造函数实例化出来的,能够根据obj.__proto__.constructor获取到。

  6. 多继承

    function Child() {
        Parent1.call(this)
        Parent2.call(this)
    }
    Child.prototype = Object.create(Parent1.prototype)
    Object.assign(Child.prototype, Parent2.prototype)
    Child.prototype.constructor = Child

    利用Obejct.assign方法将Parent2原型上的方法复制到Child的原型。

相关文章
相关标签/搜索