众所周知,在通过对象取值的时候,如果对象中没有这个属性会继续查找原型上的内容。今天就来分析如何JS中对象是如何通过原型链取到值的。
作用域链是针对函数的,原型链是针对对象的
首先是非常重要的一句话,平时函数调用中变量取值是通过作用域链,关于函数运行时的调用堆栈和作用域以后会另起文分析(函数执行上下文和this指向也会分析)。
JS中对象获取到属性值则是利用到原型链,当然原型链的作用不仅仅如此,原型原本旨在实现继承。
函数
函数也是一种对象
typeof Function
"function"
尽管如此,但是函数原型上依旧是有Object,见下图

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的原型和构造器:

对象
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
打印该对象原型:

打印的obj里面展示了a1和a2,因为对象名只是一个引用,所以后面打印的时候包括到了新添加的a2属性,.操作添加的属性会直接添加到对象上。发现除了a4都打印出来了,即对象获取是通过原型对象但是不会查找构造器中相关属性。
对象总结如下:
- 对象访问的属性是现在自身找,如果找不到就去原型上,不去constructor上找,这点和function不同
总结
- 函数.操作添加的属性值是直接添加在constructor构造器上而对象则是直接添加上
- 函数查找变量是通过原型链,具体应该是fn.prototypr.constructor里面的属性。
- 对象访问的属性是现在自身找,如果找不到就去原型上,不去constructor上找
已上就是本人通过实践的相关总结,有错误希望指正和讨论。共同努力、共同进步。