函数的原型对象
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 :>> {}
结果仍然为一个没有方法名的空对象,这里会有几个想法
- Object 在log的时候被省略了,因为
{}
就是Object- 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
的{}
(对象)。
而方法原型对象的名字是如何定义的,ES6
的name
属性应该是直接突破口。
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' }
可以看到由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
依据ECMA262
的12.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)的原型对象进行全等比较,如果出现匹配项,则该对象属于该方法类,如果一直找到原型链的顶端也没有匹配项,则不属于该类。