一道面试题展开的一点关于this和原型链的理解

55 阅读3分钟

开始

有如下一道面试题

const o = (function () {
  const obj = {
    a: 1,
    b: 2,
  }
  
  return {
    get: function (k) {
      return obj[k]
    }
  }
})()

要求是如何不修改这部分代码的情况下,做到可修改obj的属性,使得o.get('b')结果为3

思路

如果是用结果导向的思维方式,从题目的描述,期望达到的目的应该是大概可以如下伪代码所示

情况一

foo(o).b = 3

function foo (obj) {
  // 通过某些方法获取到题目中o对象中的obj属性(代理)
  // 允许修改到o的obj属性
  // 使得o.get方法可以正确访问到上述的修改
}

o.get('b') // 3

情况二

const newObj = o.get(某个代理属性)
newObj.b // 2 访问得到
newObj.b = 3 // 允许修改
o.get('b') // 3

这里主要的思路还是通过某些间接的方式(代理)使得能够从外部访问到闭包内的obj的属性,但如何做到是个大问题。闭包内部的obj是个js对象,js的对象有一个特点,访问当前实例化的对象上不存在的属性的时候,会自动的向指向上层的原型链上寻找,直到尽头(null)。

这里摘抄一段MDN的描述

const o = {
  a: 1,
  b: 2,
  // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  __proto__: {
    b: 3,
    c: 4,
  },
};

// o.[[Prototype]] 具有属性 b 和 c。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype(我们会在下文解释其含义)。
// 最后,o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null。
// 这是原型链的末尾,值为 null,
// 根据定义,其没有 [[Prototype]]。
// 因此,完整的原型链看起来像这样:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

console.log(o.a); // 1
// o 上有自有属性“a”吗?有,且其值为 1。

console.log(o.b); // 2
// o 上有自有属性“b”吗?有,且其值为 2。
// 原型也有“b”属性,但其没有被访问。
// 这被称为属性遮蔽(Property Shadowing)

console.log(o.c); // 4
// o 上有自有属性“c”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“c”吗?有,其值为 4。

console.log(o.d); // undefined
// o 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype 且
// 其默认没有“d”属性,检查其原型。
// o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null,停止搜索,
// 未找到该属性,返回 undefined。

结合一个关于this的知识点:this指向调用它的上下文对象,这里先不讨论关于this和箭头函数的情况,当o.get被调用的时候,我们如果可以构造一个不在于obj的属性,且由于访问的时候上下文对象已经指向了要被访问的obj,那么就可以在不改变obj自由属性的情况下,获得对obj的读写能力。

可能这么描述有点绕,直接上代码:

const o = (function () {
  const obj = {
    a: 1,
    b: 2,
  }
  
  return {
    get: function (k) {
      return obj[k]
    }
  }
})()

Object.defineProperty(Object.prototype, 'proxy_key', {
  get () {
    return this
  }
})

const _obj = o.get('proxy_key')
_obj.b = 3

o.get('b') // 3

这里由于第一次执行o.get('proxy_key')的时候属性并没有存在于obj内,于是自发向上层原型链,也就是Object.prototype中寻找,此刻触发了访问getter方法,此刻访问的上下文对象指向obj,如果此刻将this return,则等同于拿到了obj的访问方式。

关于这里为什么this会指向obj,可以看下面这个例子

Object.defineProperty(Object.prototype, 'proxy_key', {
  get () {
    return this
  }
})

function Person () {

}

const p = new Person()
const p_k = p.proxy_key // 向上搜寻,直到访问了Object.prototype,存在该值

// 这里访问的上下文对象是p
p === p_k // true 

总结

  • this指向执行上下文环境的对象
  • 对象的属性访问会向原型链层层向上搜寻,直至null