从instanceof运算符谈起js原型

229 阅读6分钟
前言

原型是javascript继承的基础,理解javascript原型有助于我们学习好这门语言。关于javascript的原型知识的学习,建议大家还是好好看看javascript的红宝书《javascript高级程序设计》,这是javascript界有名的大神Nicholas C.Zakas写的,正如React核心成员及Redux的创造者Dan Abramov所说的,Nicholas 对javascript的了解程度很少有人能够企及,好吧,反正经典的东西是不会过时的,每次读都能收益不少的。

本文旨在通过instanceof这个运算符的实现原原理来简单梳理一下javascript的原型知识。

一、javascript类型检测

我们知道,javascript有两种数据类型:基本类型和对象(Object)。基本类型有六种:string,boolean,null,undefined,number,symbol(ES6新增)

我们要检测基本类型,用typeof运算符:

var str = "abc",    
    num = 123,    
    flag = true,    
    symblo = Symbol(),    
    a = null,    
    b = undefined; //测试说明用,实际不建议这样定义    
typeof str // "string"
typeof num // "number"
typeof flag // "boolean"
typeof symblo // "symbol"
typeof a // "object"
typeof b // "undefined"

我们看到,除了null外其余的都能正确返回其类型,对于null,typeof 运算符返回了object类型,虽然null是属于基本类型,这是一个由来已久的bug了。另外对于Object类型,除了函数,typeof都会返回 “object”,函数返回 “function”。

var foo = function(){};
var obj = {};
var arr = [];
typeof foo; //"function"
typeof obj; // "object"
typeof arr; // "object"

我们看到,对于我们定义的arr数组,typeof也返回了 “object”,这是因为数组Array在javascript中也是对象。那么,我们如何检测出我们想要的数组类型呢?

答案就是instanceof运算符了。(当然还有其它方法,本文只讨论instanceof)

arr instanceof Array //true

instanceof 返回一个布尔类型的值,它的语法结构为

object instanceof constructor

它可以正确的判断出对象的包装类型,上面我们看到,用instanceof操作arr,返回了true,符合我们的预期。

二、instanceof内部实现机制

我们来看一下MDN上对instanceof运算符的解释:

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置

所以instanceof实际上是通过判断一个对象的原型链中是否能找到该对象的构造函数的prototype来实现的,知道了内部基本实现原理,我们就可以自己模拟实现一个instanceof功能的函数了。

   
function _instanceof(left, right) {//left表示object,right表示constructor    
  
var prototype= right.prototype, //获取构造函数的原型prototype         proto = left.__proto__; // 获取对象的原型__proto__ 
      
// 判断对象的类型是否等于构造类型的原型     while (true) {      
    if (proto === null){//找到了原型链的顶层没有找到,返回false        return false 
    }     
    if (proto === prototype){ //找到了就返回true       
        return true 
     }
     proto = proto.__proto__   // 继续返回上一层查找   }   }
}

这样我们就基本实现了一个instanceof了, 来检测一下:

_instanceof(arr,Array) // true

_instanceof(str,Array) // false

上面我们定义的arr判断是否是一个数组,返回了true,字符串str返回了false,符合预期。

三、从instanceof运算符引申出的js原型知识

从上面instanceof运算符的实现原理中我们看到,如果一个对象的__proto__等于其构造器的prototype,那么这个对象就是这个构造器的实例对象。所以上面定义的arr,实际上就是数组Array构造器new出来的一个实例对象。

var arr = new Array()
arr.__proto__ === Array.prototype  //true

在这里说一下__proto__和 prototype的关系。

__proto__和prototype都指向一个对象的原型,__proto__是javascript对象都会有的属性,而prototype是函数才有的属性,但是别忘了,在javascript中函数是一等公民,函数也是对象。函数既然是对象那么它必然也有__proto__属性,那么函数的__proto__指向谁呢?

var foo = function(){}
console.log(foo.__proto__) //ƒ () { [native code] }

在浏览器控制台打印出来的是一个匿名函数,这个匿名函数是什么呢?

我们知道,在javascript中没有类的概念(ES6中新增了class关键字,用来模拟类的继承语法,但其本质上是ES5传统继承模型的一种语法糖),要实现类继承需要通过构造函数和原型组合,换一种说法,就是用函数来模拟类

function Person(name){  
    this.name = name
}
var p = new Person("Paul")
p.name  //Paul
p.__proto__ === Person.prototype  //true

Person函数相较于foo函数,多了一个new 调用,它可以看成是一个“类”

所以到这里你应该带着出一个疑问:Person.__proto__等于什么呢?

console.log(Person.__proto__) //ƒ () { [native code] }

看到了吧,foo.__proto__和Person.__proto__都指向一个特殊的匿名函数,那么这个匿名函数到底是什么?

还记得吗,javascript中创建函数的方式除了函数声明和函数表达式,还有一个 new Function()的定义函数方式:

var fun = new Function()
typeof fun //"function"
fun.__proto__ === Function.prototype  //true

到这里你应该明白上面的匿名函数是什么了吧?其实foo函数和Person函数都是Function类的一个实例,这里也则面说明了函数也是对象的概念,因为函数都是由Function这个类new出来的啊~

foo.__proto__ === Function.prototype  //true
Person.__proto__ === Function.prototype  //true

到这里你应该会好奇,Function.__proto__又等于什么呢?答案是:

等于它自己的prototype

Function.__proto__ === Function.prototype  //true

这是比较特殊的地方,但如果你从javascript一切皆对象的角度出发,似乎也不难理解:Function也是对象啊~只不过Function是比较特殊的一个存在,它是由javascript引擎初始化的。

Function和Object的关系

我们知道,javascript一切对象皆继承自Object这个类,所以有:

var obj = new Object()

obj.__proto__ === Object.prototype  //true

Function.prototype.__proto__ === Object.prototype  //true

到这里你可能又疑惑了,Object.__proto__又等于什么呢?

console.log(Object.__proto__) // ƒ () { [native code] }

在控制台中输出我们看到,Object.__proto__与上面的foo和Person函数一样,等于一个特殊的匿名函数,所以:

Object.__proto__ === Function.prototype   //true


到这里你可能会蒙圈了,好吧,其实Object也是一个构造器,其实它和Function是一样的,都是由javascript引擎初始化的,然后通过__proto__连接起来了。那么Function和Object是谁从属于谁呢?这个其实是先有鸡还是先有蛋的问题,网上有专门介绍这两者关系的文章,有兴趣的可以去看看。这里来总结一下Function和Objcet的关系:

1. 所有构造器(包括Object,Array,String,Number,Boolean,Symbol)

的__proto__都指向Function这个类的prototype;

2. 所有对象的__proto__最终都会指向Object的prototype.


你也许还有疑惑,原型链的终点在哪?是null

Object.prototype.__proto__ === null  //true

null的本质是空的对象引用,别忘了,原型本身也是对象,原型也有它自己的原型,这就构成了原型链的概念,所以把null作为原型链的终点也是合理的,因为原型再往上查找已经没有引用了,到null这里就是终点了。