认识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' }

可以看到由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)的原型对象进行全等比较,如果出现匹配项,则该对象属于该方法类,如果一直找到原型链的顶端也没有匹配项,则不属于该类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值