前端|认识javascript原型链

函数的原型对象 JavaScript的函数内部有两个不同的内部方法
[[call]][[Construct]]
在介绍二者之前先说一下函数的原型对象,函数本身有一个原型对象,这个对象用于判定函数的类别。
以Object为例:

console.log('Object.prototype :>> ', Object.prototype); // Object.prototype :>>{}

打印的结果是一个空对象,但如果换一个普通的方法,会发现打印结果有些许的不一样
function Person() {} console.log('Person.prototype :>> ', Person.prototype); // Person.prototype :>>Person {}

与Object的结果大体相似,但多了一个方法名,但是这个方法名并不是自定义方法与系统定义方法所产生的不同,而是编译器故意为之,我们可以重新定义 Object
function Object() {} console.log('Object.prototype :>> ', Object.prototype); // Object.prototype :>>{}

结果仍然为一个没有方法名的空对象,这里会有几个想法
  1. Object 在log的时候被省略了,因为{}就是Object
  2. Object 这一个特殊对象在打印的使用还是指向系统的Object
const systemProto = Object.prototype; function Object() {} const userProto = Object.prototype; console.log('systemProto === userProto :>> ', systemProto === userProto); // true

由结果可以知道,名字为Object的方法在log的时候就是无名的,因为名叫Object的对象就是{},其他方法的原型对象都是不叫Object{}。而且重新定义方法是不会引起同名的方法所指向的原型发生变化的,也就是说,拥有不同形参的的同名方法归根结底还是一个方法。
再次证明该结论:
function Person() {} const firstProto = Person.prototype; function Person(name) { this.name = name; }const secondProto = Person.prototype; console.log('firstProto :>> ', firstProto); // Person {} console.log('firstProto === secondProto :>> ', firstProto === secondProto); // true

由于 Person的原型对象不叫Object,所以在打印这个方法的原型对象时,还告知了我们这个方法的原型对象是一个叫Person{}(对象)。
而方法原型对象的名字是如何定义的,ES6name属性应该是直接突破口。
const Person = function People(){} console.log('Person.name :>> ', Person.name); // Person.name :>>People console.log('Person.prototype :>> ', Person.prototype); // Person.prototype :>>People {}

以上的所有对比操作都是在同一栈帧下完成的,当对象处于不同栈帧时
const systemProto = Object.prototype; function test() { const userProto = Object.prototype; console.log('systemProto === userProto :>> ', systemProto === userProto); // true } test(); function anotherTest() { function Object(name) { this.name = name; } const userProto = Object.prototype; console.log('systemProto === userProto :>> ', systemProto === userProto); // false } anotherTest();

由于对象具有唯一性(按照内存地址标识),当在不同栈帧声明了同名的函数,由于其原型对象不同,函数便不再相同。
对象的原型对象 文章开头提到JavaScript函数内部有两个内部方法[[Call]][[Construct]] 当直接使用方法名执行方法时,调用[[Call]],而当使用new调用方法时,调用的是[[Construct]],而且会返回以该方法原型为基础的copy版本的实例
function Person(name) { this.name = name; }const funcResult = Person('hello'); const person = new Person('hello'); console.log('funcResult :>> ', funcResult); // undefined console.log('person :>> ', person); // Person { name: 'hello' }

【前端|认识javascript原型链】可以看到由new调用的Person方法返回了一个 Person类的{}(对象),而且,还顺带着多了一个name属性。这是因为new操作符不但拷贝了一份Person方法的原型对象,而且还执行了一遍Person方法,并以拷贝的那个Person原型对象为调用者去调用的这个方法,简化下来上述方法和下面的过程是类似的。
function Person(name) { this.name = name; }const person = Person.prototype; Person.call(person, 'hello'); console.log('person :>> ', person); // person :>>Person { name: 'hello' }

这里为什么要说类似,因为这个对象能够直接影响 Person 的原型对象
function Person(name) { this.name = name; }const person = Person.prototype; Person.call(person, 'hello'); console.log('person :>> ', person); // Person { name: 'hello' } person.age = 18; console.log('person :>> ', person); // Person { name: 'hello', age: 18 } console.log('Person.prototype :>> ', Person.prototype); // Person { name: 'hello', age: 18 }

当对person这个对象进行任何变量修改都会直接体现在Person函数的原型对象上。
那么JavaScript是如何实现new这一骚操作的,这就要引出另一个概念,对象的原型对象
虽然读起来有些拗口,但其实它和函数的原型对象是起到了同样的作用,区分对象的类别。不同点在于,对象的原型对象用来区分对象的类别,而函数的原型对象是用来区分函数的类别的。函数在定义后创建了函数的原型对象,而对象是在用new调用了函数之后生成的和函数原型对象相同类型的对象。
为什么这么说呢,以下面的代码为例:
function Person(name) { this.name = name; } const person = new Object(); console.log('person :>> ', person); // person :>>{}person.__proto__ = Person.prototype; console.log('person :>> ', person); // person :>>Person {}

其中person对象先是被声明为 Object 类型,然后我们手动的把 person对象中的__proto__属性修改为函数Person的原型对象后,person 就变成了一个 Person类型的 {},可以确定的是 __proto__属性控制了person对象的类别。
如果我们以一个实例化好的对象(有属性的)去替换这个person的原型对象,是不是它的类型就是我们实例化好的那个对象了呢?
function Person(name) { this.name = name; } const person = new Object(); console.log('person :>> ', person); // person :>>{}const p = new Person('hello world'); console.log('p :>> ', p); // p :>>Person { name: 'hello world' }person.__proto__ = p; console.log('person :>> ', person); // person :>>Person {}console.log('person.name :>> ', person.name); // person.name :>>hello world

可以看到 person的类别依然是 叫Person{},看到这里可能有些懵,但请别忘了,函数在创建时,不管有没有形参,有几个形参,函数的原型对象都是一个只有名字的{}(Object的名字省略)。而以函数的原型对象作为所有对象实例类别的判定标准,那么不管对象的内部属性如何变化,只要由同一个栈帧下的同一个名字的方法创建,就都可以将他们归属于一类。
而这里还剩下一个问题,我们的person对象实例的__proto__明明是一个具有属性的Person类对象,但为什么打印的结果没有显示出来它的name属性
这是因为,log打印的是 对象的类别 + 对象本身的属性person对象由Object创建而来的,本身是没有任何属性的,但并不意味着,p对象赋值过来的name属性从此消失了,在JavaScript执行过程中,会顺着原型链一直向上寻找属性,这一点相信总所周知了。
instanceof 依据ECMA26212.10.4Runtime Semantics: InstanceofOperator文法描述,instanceof大体流程如下
function instanceofOperator(target, v) { if (typeof target !== 'object') { return false; }const p = target.prototype; if (typeof p !== 'object') { throw new Error('type error') }while(true) { v = v.__proto__; if (v === null) { return false; } if (p === v) { return true; } } }

instanceof会从对象(v)的原型链起始端(不包括自身)开始向最顶端寻找,然后依次与目标方法(target)的原型对象进行全等比较,如果出现匹配项,则该对象属于该方法类,如果一直找到原型链的顶端也没有匹配项,则不属于该类。

    推荐阅读