对象是怎么通过原型链取到值的?

2,012 阅读6分钟

众所周知,在通过对象取值的时候,如果对象中没有这个属性会继续查找原型上的内容。今天就来分析如何JS中对象是如何通过原型链取到值的。

作用域链是针对函数的,原型链是针对对象的

首先是非常重要的一句话,平时函数调用中变量取值是通过作用域链,关于函数运行时的调用堆栈和作用域以后会另起文分析(函数执行上下文和this指向也会分析)。

JS中对象获取到属性值则是利用到原型链,当然原型链的作用不仅仅如此,原型原本旨在实现继承。

函数

函数也是一种对象

typeof Function
"function"

尽管如此,但是函数原型上依旧是有Object,见下图

所以function可以认为是特殊的对象。

        const fn = function () {
            a1 = 1;
            this.a2 = 2;
        }
        fn.a3 = 3
        fn.prototype.a4 = 4
        fn.prototype.constructor.a5 = 5

        console.log('fn: ', fn);
        console.log('fn-a1: ', fn.a1);
        console.log('fn-a2: ', fn.a2);
        console.log('fn-a3: ', fn.a3);
        console.log('fn-a4: ', fn.a4);
        console.log('fn-a5: ', fn.a5);


        const obj = new fn()
        console.log('obj: ', obj);
        console.log('obj-a1: ', obj.a1);
        console.log('obj-a2: ', obj.a2);
        console.log('obj-a3: ', obj.a3);
        console.log('obj-a4: ', obj.a4);
        console.log('obj-a5: ', obj.a5);

如上,在函数fn上面相关位置分别定义了a1-5,选择直接打印和实例化后打印,究竟哪些可以直接输出呢?

下面先揭示fn的答案:

fn:  ƒ () {
            a1 = 1;
            this.a2 = 2;
        }
fn-a1:  undefined
fn-a2:  undefined
fn-a3:  3
fn-a4:  undefined
fn-a5:  5

如上,注意到打印的fn只是定义时的样子没有任何添加,这是符合预期的。因为打印fn只会输出定义代码时,不会输出后面对其的改动。这点是function的特性和对象不同。然后a1-5只输出了a3和a5,其余都获取不到。

下面继续打印fn的原型:

打印constructor构造器:

继续打印构造器的原型对象:

仔细观察,可以发现,

  • a4定义在fn的原型和构造器同级的位置
  • a3和a5定义在构造器中
  • a4定义在构造器的原型对象上
  • 同时a1和a2是定义在函数中的

a1是作为函数fn的私有属性,只在函数内部能够访问到。

a2通过this添加,函数内部this判定比较麻烦,但是这种情况下this是指向全局的(严格模式下是undefined),并且由于函数没有执行,这种语句实际上也没有执行,也就是无法获取a2。

a3是通过.操作添加的属性值是直接添加在constructor构造器上,可以获取到,同理a5。

a4是添加在原型上,发现无法获取。

至此,对函数原型链取值作出总结:

  • 函数.操作添加的属性值是直接添加在constructor构造器上
  • 想获取函数的属性值的时候会在函数的构造器上面查找,只能获取构造器上的属性。
  • 函数查找变量是通过原型链,具体应该是fn.prototypr.constructor里面的属性。
  • 函数new的时候执行的是this相关的属性或者方法添加以及原型的绑定,没有this加持的属性和代码是不会添加到新对象上面的,但是相关属性还是可以在constructor上面看到。

fn.a 和 fn.prototype.a可以类比成静态方法和实例方法,一个可以通过静态fn直接获取到,另一个则是必须实例化之后才可以拿到。

下面看看实例化后的obj的相关输出

obj:  fn {a2: 2}
obj-a1:  undefined
obj-a2:  2
obj-a3:  undefined
obj-a4:  4
obj-a5:  undefined

打印obj的原型和构造器:

和fn的类似,但是此时obj作为对象,获取属性的方法已经和函数不同了,拿到了绑定到obj上的a2和原型上的a4。 a2是在new的过程中绑定到返回的this实例上的,a4则是处在和构造器同级的原型上,关于对象的规律下面在对象中一起说明。

对象

const object = {
    a1: 1
}
object.a2 = 2
Object.getPrototypeOf(object).a3 = 3
Object.getPrototypeOf(object).constructor.a4 = 4

console.log('obj: ', object);
console.log('obj-a1: ', object.a1);
console.log('obj-a2: ', object.a2);
console.log('obj-a3: ', object.a3);
console.log('obj-a4: ', object.a4);

在对象object中直接定义属性a1,后面又通过.操作添加a2,注意,尽管对象的原型属性__proto__属性每个浏览器都实现了,但是不推荐使用,建议使用Object.getPrototypeOf方法获取到原型对象。

已废弃 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。 ----MDN

警告: 通过现代浏览器的操作属性的便利性,可以改变一个对象的 [[Prototype]] 属性, 这种行为在每一个JavaScript引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.proto = ... 语句上, 它还会影响到所有继承来自该 [[Prototype]] 的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承 [[Prototype]] 的对象,推荐使用 Object.create()。 ----MDN

警告: 当Object.prototype.proto 已被大多数浏览器厂商所支持的今天,其存在和确切行为仅在ECMAScript 2015规范中被标准化为传统功能,以确保Web浏览器的兼容性。为了更好的支持,建议只使用 Object.getPrototypeOf()。 ----MDN

下面展示打印结果

obj:  {a1: 1, a2: 2}
obj-a1:  1
obj-a2:  2
obj-a3:  3
obj-a4:  undefined

打印该对象原型:

注意a4在构造器中,因为无关属性太多不再打印,后续会详细介绍对象和函数的原型和构造器。

打印的obj里面展示了a1和a2,因为对象名只是一个引用,所以后面打印的时候包括到了新添加的a2属性,.操作添加的属性会直接添加到对象上。发现除了a4都打印出来了,即对象获取是通过原型对象但是不会查找构造器中相关属性。

对象总结如下:

  • 对象访问的属性是现在自身找,如果找不到就去原型上,不去constructor上找,这点和function不同

总结

  • 函数.操作添加的属性值是直接添加在constructor构造器上而对象则是直接添加上
  • 函数查找变量是通过原型链,具体应该是fn.prototypr.constructor里面的属性。
  • 对象访问的属性是现在自身找,如果找不到就去原型上,不去constructor上找

已上就是本人通过实践的相关总结,有错误希望指正和讨论。共同努力、共同进步。