JavaScript中的原型链

162 阅读5分钟
前言

面向对象编程强调的是数据和操作数据的行为,面向对象中类是对世间万物对抽象,而类的设计模式是实例化、继承和多态。JS有对象的概念,却不像其他那样面向对象对语言那样,有类的概念,加上其他的一些原因,会有JS到底是面向对象还是基于对象的争论。

提到面向对象,最成功的恐怕就是诸如C++和Java这些使用类的语言了。而JS,没有使用类,而是使用了原型这个概念。

类和原型的区别,从我的理解来看,类倾向于描述类与分类与实例之间的关系,如交通工具=>车=>小汽车,每个对象实例都需要预先定义一个类然后实例化;而JS,抛开那些为了模仿JAVA的特性,JS更倾向关于对象实例与对象实例之间的关系,基于一个通用模版来描述对象实例,如小汽车和自行车的通用模板就是有轮子,每一个对象实例都可以成为另一个对象的原型。

在JS中,只要是有相似行为的对象都可以使用原型链的方式将之连接起来,而基于类的语言,不会这么随心所欲,必须遵从类与分类与实例的关系,这与类是有很大的区别。也可以说,原型这个机制的本质是对象与对象之间的关联,而非类这种从属关系。

(即便ES6当中新增了class这个关键字,但这本质上还是模拟类的语法糖,并不意味着JS就有了类)

1、关于原型链

不论什么语言,对象的本质上都有共通之处,唯一性或者状态或者行为。唯一性表现在内存地址的不同上,状态则表现为对象的属性,行为则表现为对象的方法。

在JS中,对象是属性名和属性值的集合,这个属性值可以是JS的任一数据类型,这些属性值是我们可以遍历出来。除此之外,对象还会拥有一个[[prototype]]属性,这个,就是对象的原型。同时,任何对象都有原型,前言说过对象的原型可以是另一个对象,在原型对象中又有原型,这样,就有了原型链这个概念,不管如何指定原型,原型链的尽头是Object.prototype

[[prototype]]这个属性是不可见的,不过浏览器厂商为对象添加__proto__,用__proto__实现了[[prototype]]属性,导致我们可以在控制台直接观察到对象的原型,后来ECMA也把这个既成事实纳入了规范当中

 var test = {}
 
 var a = Object.create(test)
 
 //b的[[prototype]]为test对象,test()的[[prototype]]为Object.protype,这就是一条完整的原型链
 //new操作符也是这样的原理
 //原型链跟作用域链有些相似
 

所以我们使用对象的一些通用方法时,就是通过原型链查找到Object对象内置的方法上。

然后,来说说new这个操作符,到底发生了什么。

首先,JS虽然提供了new这个操作符,但前面说过JS并没有类的定义,new操作符是模仿JAVA的产物,所以JS的new也不是对一个类的实例化,而是通过构造函数来初始化一个对象,并把这个对象返回。

任何一个函数都可以使用new操作符来构建一个对象。当我们定义好一个函数之后,这个函数会自动创建一个名为constructor对构造函数,constructor指向对是这个函数本身。我们使用new操作符来调用这个函数,就是调用constructor来构建一个对象。

//这里可能会有一个疑惑,既然函数的constructor指向函数本身,那直接调用这个函数,不也是一样的结果吗?

function Test(){} //定义一个空白函数

Test.prototype.hello = function () { return true } //在函数的原型上添加一个hello方法,返回值为true

var a = Test() 

console.log(a)  //console.log(undefiend)

//这里的Test()执行后并没有任何返回值,所以a是undefined;相应的,a.hello()就是TypeError了,无法从undefined上查找原型链

var b = new Test()

console.log(b)  //console.log({})

//这里打印了一个空对象,这是new操作符创建了一个空对象并返回。

console.log(b.hello()) //console.log(true)

// 这是因为b的原型链为Test的[[prototype]]对象,于是根据原型链查找规则找到hello


使用new来调用Test函数会创建一个全新的对象,这个新对象会被绑定到函数的执行上下文,然后通过Test的constructor构造函数来初始化这个新对象,到最后把这个新对象返回,假如在函数内手动返回一个对象,那么new操作符创建对对象将会被丢弃。

JS是一门具有高动态性的语言,可以在原型当中随意增删属性,原型的复制有两种思路,其一是真实地复制了原型对象,其二是像JS这样只是复制了原型对象的引用,这一点很重要。

//使用Object.create(obj)创建一个实例
var test ={
    a :0
}

var objTest = Object.create(test)

console.log(objTest.a) //console.log(0)

test.a = 1

console.log(objTest.a) //console.log(1)

//使用test.a的值会影响到objTest,而objTest.a并不会修改test的值;
//这是因为test.a=1后,console.log(objTest.a)查询a使用的是原型链查找规则;
//objTest=2是在objTest这个对象当中添加了a属性并赋值,然后console.log(objTest.a)是打印objTest中a的值,而非原型链test上的。
//有一个要注意的点,objTest中有和test对象同名的方法,这并不意味发生了多态,而是在“实例对象”中添加了一个同名函数,遮蔽了test中的方法。

这里说明了JS到原型复制思路是引用复制,实际上objTest的原型对象和test对象指向的是同一个内存地址。

最后提一下ES6中模拟类的方式。

class Test {
    constructor (...args){
        //...
    }
    
    functionTest(){}
    //...
}

var test =Test()


//这种方式只是让我们的代码看起来优雅了,本质上,依然是模拟类的方式。
// 在控制台中可以console.log(test) ,观察到test的__proto__属性,与ES5中的模拟类,并无区别。