javaScript深入解析2-原型及原型链

关于原型(prototype)

原型可以说是JavaScript设计中最特殊,也可以说是最精髓的设计,他遍布整个JavaScript对象。几乎也是整个JavaScript技术的核心,只有熟悉了原型,才能充分理解JavaScript中的类与委托机制了。

what

Q:什么是原型呢?

A:在所有对象中都有一个特殊的**[[Prototype]]属性**,其实就是对其他对象的引用。几乎所有的对象在创建时[[Prototype]]都会被赋予一个非空的值。(注意,部分浏览器将该属性暴露出来,并命名为 _proto_)

Q:什么是原型链呢?

A:如上所说,每一个对象都会有一个[[Prototype]]属性,然而[[Prototype]]属性引用的也是一个对象,这个对象同样也会含有[[Prototype]]属性,这样每一个对象就如同一个链节,一起就组成了一个链,我们称之为原型链

why

Q:为什么要设计这样一个独特的机制呢?

A:或许不应该对存在这个问题如此深究,因为这可能是第一代开发人员的灵光一现,就有了这个特征。但实际上大都认为这是一门动态编译语言,或者说是脚本语言,所以没有设计类的概念,但为了填补这个概念,就有了原型。但谁也不知道JavaScript会在接下来的几十年中发展的如此蓬勃,程序员们多么希望JavaScript有OOP的概念,于是想方设法的使用原型链来模拟类的行为,但无论如何,就现在为止,JavaScript底层是没有类的概念的,包括ES6的类,任然是原型的语法糖

how

我们了解了什么是原型,为什么会存在原型之后,就要知道我们该如何使用它了。

创建对象

我们来看一下创建对象的几种方法。

  1. let obj = {num : 0}
  2. let obj = new Object({num : 0}) 或者var obj = Object({num : 0})
  3. let construt = function(){this.num = 0} => let obj = new construct()
  4. let obj0 = {num : 0} => let obj1 = Object.create(obj0)

这几种方法创建的对象有所不同,我们还是来解析一下:

1,2:字面量方法,创建的是一个普通对象,其[[Prototype]]指向Object.prototype(大部分对象的原型链最后都指向它)。

3:new操作符:

当我们看到new操作符,一定会想到类,但是JavaScript中的new操作符与传统OOP语言中的new完全不同,JavaScript中的new只是将函数当成一个构造函数来调用。如同上一篇中所说,JavaScript中所有的函数都一样,不存在本质的构造函数,只有被new调用的函数就是构造函数。我们在回顾一下new操作符的执行过程:

1
2
3
4
5
6
7
8
9
function C(name){
this.name = name;
}

let obj = new C('mw');

console.log(obj);
//C {name:'mw'}
//此时obj的__proto__指向的是C.prototype
  1. 创建一个新的对象
  2. 将新对象的[[prototype]]设为C.prototype
  3. 将新对象设为函数调用的this
  4. 如果函数没有返回其他对象,则new操作符调用的函数会自动返回这个新对象,否则则会直接返回函数内的对象

4: 使用Object.creat()函数来创建对象,实际上是创建了一个新对象,然后将其[[Prototype]]设置为对应对象,如果不加参数,则[[Prototype]]为Object对象。

1
2
3
4
let a = {num : 0};
let b = Object.create(a);
console.log(b.__proto__);
//{num:0,__proto__:Object}

属性获取,设置与屏蔽

当我们有了一个对象,我们可能会对他们做一些操作,比如,[[get]],[[set]],或者seal,freeze等等操作。那么这个过程又是什么样的呢?

我们提前需要了解的是:每一个对象的每一个属性都是具有属性描述

数据属性

  • configurable:表示该属性能否通过delete操作符删除从而重新定义。默认为true。
  • enumerable:表示该属性能够通过for-in循环遍历返回属性值。默认为true。
  • writable:表示能否修改属性值。默认为true。
  • value: 包含这个属性的数据值,读属性时,从这个位置读;写属性的时候,把新值保存在这个位置。默认为undefined。

访问器属性

  • configurable:表示该属性能否通过delete操作符删除从而重新定义。默认为true。
  • enumerable:表示该属性能够通过for-in循环遍历返回属性值。默认为true。
  • get:读取属性时调用的函数。默认为undefined。
  • set:写入属性时调用的函数。默认为undefined。

值得注意的是:

  1. 官方来说,只能通过Reflect.defineProperty(obj,proName,proValue),但是Firefox在最开始指定了__defineGetter__,__defineSetter__,后期Chrome,Opera,Safari也实现了该方法。
  2. 在defineProperty()中get,set不能与writable,value,同时设置。
  3. 在ES6中,将Object中的很多方法(包括defineProperty…)都放到了Reflect对象中,虽然Object任然包含该方法,但建议使用Reflect对象。

属性获取与屏蔽

思考一下下面的代码:

1
2
3
4
let obj = {num : 0};
let str = obj.toString();
console.log(str);
//"[object Object]"

首先我们需要思考的是,我们并没有在obj上定义toString方法,为什么我们能够调用,并且得到一个结果(虽然并不那么漂亮),我想你肯定猜到了,那是因为obj的原型对象Object拥有这个方法。所以,我们在或得一个对象的值时,是从链的底端开始查找,顺着原型链,一直查到顶端,如果不存在,返回undefined或者error。如果存在,则返回这个值。也就是说,我们能够在一个对象中查找到值,并不代表该对象含有该值,很有可能是该对象的原型链上的某个原型对象含有该值。

思考下面的代码:

1
2
3
4
5
let a = {getString:function(){return 'from a'}};
let b = Object.create(a);
b.getString = function(){return 'from b'};

console.log(b.getString());

结果是什么呢?hava a try!

没错,结果是’from b’

这就是我们所说的属性屏蔽这一切的根源在于:我们是从链底查到链顶的,所以下层的同名属性会优先被获取,一旦引擎获取到该属性,则遍历结束,不会再向下查找。

属性设置与屏蔽

当我们为一个对象的键赋值时,会发生三种情况:

eg:

1
2
let obj = {};
obj.name = 'value'
  1. 如果在[[Prototype]]链上存在名为name的普通数据访问值,并且被标记为可写(writale:true)(默认即为true),那就会直接在obj上添加一个名为name的新属性,它就是屏蔽属性
  2. 如果在[[Prototype]]链上存在名为name的普通数据访问值,但是该属性被标记为只读,(writable:false),那么无法修改已有属性或者在obj上创建屏蔽属性。如果在严格模式下,会抛出一个错误;否则会忽略该语句。总之,不会发生屏蔽
  3. 如果在[[Prototype]]链上存在name并且它是一个setter。那就一定会调用这个setter。name不会被添加到obj上,也不会重新定义setter。

屏蔽的发生也许不像我们通常认为,还要联系对象属性的描述符来确定!我们需要记住。

隐式屏蔽

正如类型转换一样,同样存在隐式屏蔽,只要操作对象的属性,都有可能发生隐式屏蔽。

比如:

1
2
3
let obj0 = {num:0};
let obj1 = Object.create(obj0);
obj1.num++ //这里发生了隐式屏蔽!!!

只要是[[set]],都有可能发生隐式屏蔽

修改[[Prototype]]

  1. Object.create(),该方法是最适用的方法。即在创建时就设置其prototype。

    1
    2
    3
    let obj0 = {num:0};
    let obj1 = Object.create(obj0);
    //将obj0设置为obj1的prototype
  2. Object.setPrototypeOf(),该方法是ES6新增的方法。

1
2
3
4
let obj0 = {num:0};
let obj1 = Object.create(obj0);
Object.setPrototypeOf(obj0,obj1);
//把obj1设置为obj0的protoype
  1. 构造函数方法

    1
    2
    3
    let F = function(){this.a = 123};
    let obj = {};
    obj.prototype = new F();

回顾一下上面new操作符,就能想明白。

检测原型链

我们又该如何检测对象的原型连上存在哪些原型对象呢?

  1. instance操作符
1
2
3
let obj = {};
console.log(obj instance Object);
true
  1. __proto__属性,如上所说,大多数浏览器实现了__proto__(在ES6中加入了标准),在兜底情况时,可以使用这种方法检测。

Last

理解清楚原型链,我们才能清楚理解后面的委托,“类”等等。所以这一部分是基础。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :