函数的原型对象 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' }
【前端|认识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 依据
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)的原型对象进行全等比较,如果出现匹配项,则该对象属于该方法类,如果一直找到原型链的顶端也没有匹配项,则不属于该类。推荐阅读
- 操作系统|[译]从内部了解现代浏览器(1)
- web网页模板|如此优秀的JS轮播图,写完老师都沉默了
- 接口|axios接口报错-参数类型错误解决
- JavaScript|vue 基于axios封装request接口请求——request.js文件
- vue.js|vue中使用axios封装成request使用
- JavaScript|JavaScript: BOM对象 和 DOM 对象的增删改查
- JavaScript|JavaScript — 初识数组、数组字面量和方法、forEach、数组的遍历
- JavaScript|JavaScript — call()和apply()、Date对象、Math、包装类、字符串的方法
- JavaScript|JavaScript之DOM增删改查(重点)
- 前端|web前端dya07--ES6高级语法的转化&render&vue与webpack&export