资讯专栏INFORMATION COLUMN

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

lentrue / 1459人阅读

摘要:从原型对象指向构造函数画一条带箭头的线。线上标注,表示该原型对象的构造函数等于。但除此之外,若构造函数所指的显示原型对象存在于的原型链上,结果也都会为。执行构造函数,并将指针绑定到新创建的对象上。

做前端开发有段时间了,遇到过很多坎,若是要排出个先后顺序,那么JavaScript的原型与对象绝对逃不出TOP3。

如果说前端是海,JavaScript就是海里的水

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

现在的软件开发,很少有不是面向对象的,那么JavaScript如何创建对象?
一、 创建对象的方法

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

对象字面量

var object1 = { name: "object1" }

构造函数法

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

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

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

var Parent = { name: "Parent" }
var object4 = Object.create(Parent)

想要明白JavaScript原型继承的幺蛾子,势必要搞清楚原型对象、实例对象、构造函数以及原型链的概念和关系,接下来我尽量做到表述地结构清晰,言简意赅。
二、原型继承

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

画一个等边三角形,从顶点顺时针为每个角编号(1)、(2)、(3)

其中(1)旁边标注“原型对象”,(2)构造函数,(3)实例对象

从(2)构造函数(如上节例中的ClassMethod)指向(3)实例对象(上节例中的object2)画一条带箭头的线。线上注明new运算符,表示var object2 = new ClassName()

从(2)构造函数指向(1)原型对象画一条带箭头的线。线上标注prototype,表示该构造函数的原型对象等于ClassName.prototype。(函数都有prototype属性,指向它的原型对象)

从(3)实例对象指向(1)原型对象画一条带箭头的线。线上标注__proto__,表示该实例对象的原型对象等于object2.__proto__,结合第4步,便有ClassName.prototype === object2.__proto__

从(1)原型对象指向(2)构造函数画一条带箭头的线。线上标注constructor,表示该原型对象的构造函数等于ClassName === object2.__proto__.constructor

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

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

人们总是习惯通过熟悉的事物,类比去认识陌生的事物。这或许是一种快速的方式,但这绝对不是一种有效的方式。类比总会让我们轻视逻辑推理
三、从instanceof再看原型链

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

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

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

四、new运算符
new Foo()执行过程中,都发生了什么?

以下三步:

创建一个继承自Foo.prototype的新对象。

执行构造函数Foo,并将this指针绑定到新创建的对象上。

如果构造函数返回一个对象,则这个对象就是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
    }
}
五、实现继承
在ES6中,出现了更为直观的语法糖形式:class Child extends Parent{},但这里我们只看看之前没有这种语法糖是怎么实现的。我一直有一个体会:要想快速的了解一个事物,就去了解它的源起流变

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

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

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

构造函数方式

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

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

原型链方式

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

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

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

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

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

组合优化方式

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

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

终极方式

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获取到。

多继承

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的原型。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94484.html

相关文章

  • 前端竹节》(2)【正则表达式】

    摘要:一正则使用分类正则表达式后文简称为正则可划分出两种使用方式通过正则字面量与通过构造函数创建出来的正则对象,在不考虑访问正则对象属性的情况下,是等价的。匹配前一个表达式次或多次。 正则表达式在前端开发中,对于字符串处理任务来说,绝对是一件可以祭出的大杀器。同时对于前端开发人员来说也是一项基本技能,但若只是停留在能看懂,知道去哪查的阶段,那距离得心应手地运用差的可能不止一步两步。 行业总习...

    xiaowugui666 评论0 收藏0
  • 前端竹节》(1)【跨域通信】

    摘要:一同源策略用户浏览网站时难免需要将一些经常用到的信息,缓存在本地以提升交互体验,避免一些多余的操作。无法获得请求不能发送同源策略是必要的,但这些限制有时也会对一些合理的使用带来不便,这便引出了跨域通信的需求。 一、同源策略 用户浏览网站时难免需要将一些经常用到的信息,缓存在本地以提升交互体验,避免一些多余的操作。那么这些信息中难免有些就会涉及用户的隐私,怎么保证用户的信息不在多个站点之...

    Lycheeee 评论0 收藏0
  • 重学前端学习笔记(八)--JavaScript中的原型和类

    摘要:用构造器模拟类的两种方法在构造器中修改,给添加属性修改构造器的属性指向的对象,它是从这个构造器构造出来的所有对象的原型。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入winter的专栏学习【原文有winter的语音】,如有侵权请联系我,邮箱:kai...

    nanfeiyan 评论0 收藏0
  • 重学前端学习笔记(八)--JavaScript中的原型和类

    摘要:用构造器模拟类的两种方法在构造器中修改,给添加属性修改构造器的属性指向的对象,它是从这个构造器构造出来的所有对象的原型。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些要点笔记以及感悟,完整的可以加入winter的专栏学习【原文有winter的语音】,如有侵权请联系我,邮箱:kai...

    k00baa 评论0 收藏0

发表评论

0条评论

lentrue

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<