JS原型链

  • A+
所属分类:Web前端
摘要

@​ JavaScript的对象通过其身上原型这一点特征,来实现与传统的面向对象编程语言截然不同的继承机制

@

原型链

​ JavaScript的对象通过其身上原型这一点特征,来实现与传统的面向对象编程语言截然不同的继承机制

prototype)原型是什么?

  • 在JS中大多数情况下创建的对象,都拥有一个原型对象,创建的对象,从其原型为模板、来继承方法和属性。而原型对象本身都有可能拥有原型,并从中获取方法和属性,以此类推,这种关系便被称为原型链原型链的关系解释了为何一个对象身上会拥有本不属于他的属性和方法。
  • 所有拥有原型的对象的最顶层便是Object对象的prototype

prototype长什么样子

​ 先复制如下代码放入到浏览器的控制台回车查看结果:

function test(){} console.log(test.prototype); 

​ 放入到控制台打印的结果为:

JS原型链

这里我把其中的关系做了个图来方便理解,忽略其他属性,专注于constructor和__proto__属性

JS原型链

  • __proto__== prototype 这两个是一样的,你调用test.prototype和调用test.___proto___是一样的
  1. 通过上面我们发现当调用test.prototype时,会出现两个属性:constructor__proto__

    1. constructor: 该属性指向了用于构造此实例对象的构造函数,在上例中就为function test(){}
  2. 我们会发现test.prototype.__proto__属性的引用指向Object的原型对象

  3. Object的原型对象里面同样包含了:constructor、__proto__属性,只不过Object的__proto__属性的值为NULL,这跟我们前面说的:所有拥有原型的对象的最顶层便是Object对象的prototype,往后就没有原型对象了,所以才会出现为NULL的情况

添加属性

现在让我们在test的原型上添加一个name属性

test.prototype.name = 'zhang'; function test(){} console.log(test.prototype); 

JS原型链

我们可以发现在test的原型上多一个名为name的属性

我们继续来添加属性,这次我们要实例化test的对象,需要用到new关键字

var obj = new test(); 

然后我们试着在obj对象里面添加一些属性,然后再看整体的原型结构是什么样子的

test.prototype.name = 'zhang'; function test(){} var obj = new test(); obj.age = 13; console.log(test.prototype); 

JS原型链

这里我把其中的关系做了个图来方便理解,忽略其他属性

JS原型链

目前的结构为:obj对象里面有一个age属性,而test.prototype原型上一个name属性

访问原型属性 初探原理

正常情况下,当我们访问一个对象里不存在的属性时,会返回undefined

JS原型链

下面这一段代码让我们来看看结果是什么:

test.prototype.name = 'zhang'; function test(){     this.age = 12; } var obj = new test(); console.log(obj.name); 

JS原型链

通过控制台打印我们发现结果为“zhang”

对于这个结果我们应该有疑惑比如:

  1. 为什么在test对象里面并没有定义名为name的属性,可是实例化之后我们却可以访问,并获取其结果,为什么不是undefined

我们可能想到了于上文的原型有关,让我们再来看段代码:

console.log(obj.__proto__); console.log(test.prototype); 

让我们来看看打印结果

JS原型链

可以发现obj.__proto__test.prototype的打印结果是一致的

到这里我们应该也有些眉头了

原型链的查找

  1. 当我们访问obj.name时,浏览器首先查找test对象本身是否有这个name属性,如果有就会直接拿来进行使用,例:

JS原型链

  1. 如果obj没有这个name属性,那么浏览器就会从obj的__proto__中查找这个属性,在这里的obj.__proto__等同于test.prototype

  2. 如果obj.__proto__上有这个name属性,那么就会获取他,就如上例打印所示,在test.prototype上面有一个name属性值,那么我们打印obj.name 就会取到test.prototype上面的属性值

  3. 如果test.prototype对象上也没有name属性值,那么我们就会继续往上一个__proto__上去找具有name属性值的prototype,比如obj.proto.proto,在本例中,第三层就已经是Object对象了,在实际的例子中可以会有多层

  4. 如果Object.prototype上面也没有name属性,那么最终就会返回undefined,如下图所示:

JS原型链

上面的关系用一张图总结
JS原型链

理解原型对象

有如下代码,我们来看看

function test(){     this.name = 12; } var obj = new test(); console.log(obj.name); console.log(obj.toString());  

JS原型链

这个toString()方法是哪里来的纳?结合我们上文理解其实不难想到,应该是原型链中某一个环节里面的方法

这这个例子中有如下过程:

  • 浏览器首先检查obj对象里面是有可以使用的toString()方法
  • 如果没有可以使用的toString()方法,浏览器会查看obj对象的原型对象(即test构造函数的prototype属性),是否有可用的toString()方法
  • 如果也没有,浏览器会继续往上寻找,在本例中就为obj.proto.proto(即Object的prototype属性)
  • 我们在Object的prototype属性里面找到了我们要使用的toString()方法,于是我们就会看到这个方法被调用的结果

注意:原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式。

没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中,在 JavaScript 语言标准中用 [[prototype]] 表示。然而,大多数现代浏览器还是提供了一个名为 __proto__ (前后各有2个下划线)的属性,其包含了对象的原型。你可以尝试输入 实例化对象.__proto__实例化对象.__proto__.__proto__,看看代码中的原型链是什么样的! ---以上引自MDN

如何定义一个可以被继承的属性或者方法?

我们来看一下Object对象

JS原型链

我们可以发现他的prototype里面就有我们之前案例的toString方法,但是在他的prototype外面有一些类似于create等方法

我们来段代码:

function test(){ } var obj = new test(); console.log(obj.toString()); console.log(obj.create());  

结果如下:
JS原型链

系统提示我们obj.create并不是一个函数,但是obj.toString是可以输出结果的,他们之间在这里的区别只有toString是在Object的原型里面定义的,而create方法是Object对象内部定义的方法,只有他自己是可以访问的

那么讲到这里,我们就可以定义的原型的方法和属性被继承

function test(){ } test.prototype.name = '我要被继承了'; test.prototype.toInt = function(){     console.log('我是toInt'); } var obj = new test(); console.log(obj.name); console.log(obj.toInt()); 

JS原型链

我们成功的利用test上定义的原型方法和属性,使obj对象成功的获取到

原型查找、重写的就近原则

如果我们的代码是这样的,会出现什么结果纳?

function test(){ } Object.prototype.name = '我是Object的name属性' test.prototype.name = '我是test的name属性'; test.prototype.toSring = function(){     console.log('我是test原型上面的toString方法'); } var obj = new test(); console.log(obj.name); console.log(obj.toSring());  
  1. 我们先是在Object.prototype的原型对象上定义了名为name的属性,并赋值
  2. 我们又在test.prototype的原型对象上也定义了名为name的属性,并赋值
  3. 我们在已知Object的原型对象上有一个名为toString的方法,但是我们又缺心眼的在test.prototype的原型的对象上也定义了一个toString方法
  4. 那么当我们调用obj.name、obj.toString() 结果是怎么样的纳
    JS原型链

结果是我们的name和toString全部执行的test原型对象里面的定义的方法和属性,这里就引出了原型链查找时的就近原则

Object是在test原型链的上层,所以当我们调用name时会先使用test原型对象的属性,如果没有再往上进行查找

同时这里也出现了我们定义的方法和Object原型对象里面的定义的方法,名字相同的情况下,这种情况下,我们在原型链底层定义的与顶层同名的函数时,底层的方法会覆盖顶层同名的函数(也称为重写),所以当调用同名函数时,按照原型链的就近规则,我们会取离得最近的原型对象里面的同名函数(这里的顶层可以理解为树的根节点,也就是Object)

在这里我们如果直接调用Object.toString()方法

JS原型链

是不出现test定义的toString,因为我们是直接越过了底层原型链进行的顶层原型链的调用

构造函数里面的this指向问题

现在让我们回归到test构造函数里面的来看看this指向的问题

函数和构造函数他们之间其实没有什么区别,但是一个可以实例化一个不可以是为什么?

其实主要是看new这个关键字,让我们来看如下的代码:

function test() {     console.log(this);     this.name = 'name'; } test.prototype.age = 123; var obj = new test(); 

让我们来看看结果:

JS原型链

这里的打印的this是一个对象,对象里面的prototype属性有我们在test原型上定义的age属性和constructor,这里我画个图来方便大家理解

JS原型链

  1. this首先是一个对象,当你在test()构造函数内部用this.属性定义属性会放在图中蓝色区域,对象结果的name的同一层
  2. 而__proto__对象里面放的就是test原型链里面定义的属性比如:age
  3. 这里在原型链寻找中,当我们没有在test里面找到age属性时,就会隐式的调用__proto__属性里面的age属性

JS原型链

  1. 我们打印obj会发现其结构是与this相同的,其实在我们new 对象时,就已经把这个this的值,传给了new 对象时要赋值的变量

this是哪里来的

这个this是哪里来的纳?

其实当我们new 对象时,在构造函数内部会有隐式的几部操作

function test() {     // var this = {     //     __proto__: test.prototype     // }     console.log(this);     this.name = 'name';     // return this; } test.prototype.age = 123; var obj = new test(); 

当我们看到这几部操作时,我想就都明白了为什么我们在构造函数内部定义属性需要this来进行赋值

一些关于原型的常用操作

Object.create

  • Object.create(参数) 可以指定的参数(原型对象)创建一个新的对象

示例如下:

function test() {  } test.prototype.age = 123; var obj = new test(); var obj2 = Object.create(test); console.log(obj); console.log(obj2); 

JS原型链

我们可以发现Obj2的__proto__指向的原型对象为obj,而obj的原型对象为test,create相当于对指定的原型对象下面再添加一层链的关系

这里Object.create(null)的参数是可以为null的

JS原型链

我们可以发现obj2是没有原型的,所以这也是文章开始时说的大部分对象都是有原型的

constructor 属性

  • 每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。

案例如下:

function test() {  } test.prototype.age = 123; var obj = new test(); console.log(obj.constructor); 

JS原型链

这里我们看到obj.constructor返回的是test构造函数

JS原型链

  1. 第一个输出是因为我们创建了对象,打印出了param参数,但是因为没有参数传递,所以为undefined
  2. 第二次是我们的打印的constructor
  3. 这里返回的constructor因为是一个对象,所以我们可以在后面加上()来实例化这个对象,这里我们实例化了他,并传递了参数12132,所以他第三次打印出了参数,因为已经成功到创建了对象

原型的增删改查

    • 这里我们可以在原型底层实例化对象中,直接.属性或者方法,让浏览器隐式的帮我们查找,也可以直接显示的查找他们的__proto__属性
    • 构造函数.prototype.属性/方法 = 值	 
    • 构造函数.prototype.属性/方法 = 值	 如果嫌一个个赋值太麻烦,那么我们可以直接赋值一个对象 构造函数.prototype = { 	key:vlaue, 	name:'1123' } 
    • delete 构造函数.prototype.属性/方法