前言
作为Javascript的基础之一,原型一直贯穿我们的JS代码并且成为面试的常考问题。用我自己的话去理解,原型就是类,通过创建(new)一个实例的方法去继承这个类的属性跟方法,而原型链就是一个不断继承的链条,到了*(Object)会用null去终止这个链条的延伸。
原型
什么是原型
The prototype is an object that is associated with every functions and objects by default in JavaScript, where function's prototype property is accessible and modifiable and object's prototype property (aka attribute) is not visible. Every function includes prototype object by default
用官方的解释就是
在JavaScript中,prototype对象的继承性是实现面向对象的一个重要机制。
每个函数就是一个对象,函数对象都有一个子对象 prototype对象,类是以函数的形式来定义的。prototype表示该函数的原型,也表示一个类的成员的集合
用java的类概念去理解
初学的时候,网上不少人用一张错综复杂的图去诠释原型链的关系,然而对于涉猎未深的同学,这无疑是灾难般的入门方法。而我期望各位先把概念理清,再用一张最基础的图循序渐进地理解原型链的关系。
学过面向对象的同学都会用类去定义一个通用的类型,如此一来,我们创建一个新的实例,便继承了类(Kitten)的方法
public class Kitten{
int kittenAge;
public kitten(age){
kittenAge = age;
}
public void roar() {
System.out.println("喵喵喵");
}
}
复制代码
在这里直接创建了一个实例(example),可以看到实例继承到了父类(Kitten)的属性跟方法
Kitten example = new Kitten(12);
System.out.println(example.kittenAge); // 12
example.roar(); // 喵喵喵
复制代码
用函数实现构造函数
Javascript并没有class这种关键字,虽然我是用class去理解构造函数,但实际上Javascript是用函数去实现
补充:ES后补充了class的写法,但这里只用function做栗子
function Kitten(age){
this.kittenAge = age
this.say = function(){
console.log('喵喵喵')
}
}
const example = new Kitten(33)
console.log(example.kittenAge) // 33
复制代码
example 就是构造函数(Kitten)的实例,它继承了Kitten的属性跟方法
理解prototype跟_proto_,constructor
先了解几个规则:
- 引用类型,都有一个隐性原型( __proto__)的属性,指向一个普通的对象
- 引用类型的隐性原型指向其构造函数的显性原型(prototype)
- 在获取对象的属性时,如果它内部没有这个属性,它会去隐性原型去寻找(也就是其构造函数的显性原型)
prototype的中文翻译就是原型,一般称之为显性原型,这个属性只是添加给构造函数用,可以通过prototype去增加属性或方法,实例没有显性原型这个属性. 而_proto_被称为隐性原型,所有引用类型都有隐形原型
要充分了解原型链,我们要特别留意上述的规则2:
function Kitten(age){
this.kittenAge = age
this.say = function(){
console.log('喵喵喵')
}
}
const example = new Kitten(33)
const obj = {};
example.__proto__ === Kitten.prototype
obj.__proto__ === Object.prototype
复制代码
以及规则3:obj没有toString()的方法,它会沿着构造函数Object的显性原型去寻找
const obj = {a : 1}
obj.toString() // [object Object]
复制代码
constructor
这个单词的直译就是构造函数,上面我们用prototype指向构造函数的原型,那么这里就可以用constructor去指向原型的构造函数,:
function Kitten(age){
this.kittenAge = age
this.say = function(){
console.log('喵喵喵')
}
}
const example = new Kitten(33)
Kitten.prototype.constructor === Kitten // true
复制代码
用一张最直观的图来显示三者的关系 来源于juejin.cn/post/684490…
必须留意的是,最后的Object的prototype的隐形原型__proto__指向null,是为避免出现原型链的死循环
理解instanceof
想起我刚入行时面试官问我的一个问题
“用什么方法可以判断数据类型?”
“typeof”
“那可以判断出普通对象(Object)跟数组(Array)吗?”
“可以”
结果就是面试官留下了冷冷的不屑,然后我一结束面试马上check了一下。typeof只能判断基本类型,但不能精确判断引用类型。这时候instanceof就发挥作用了,而它就是利用原型链的原理,一直往上寻找,当链上出现xxx.prototype(如Object.prototype)时,就会判断为true
代码举例
const person = new Person()
const arr = [1,2,3]
console.log(person instanceof Person) // true
console.log(arr instanceof Array) // true
console.log(Person instanceof Object) // true
console.log(person instanceof Object) // true
复制代码
上面用了四个instanceof方法去判断对象跟数组
- 根据原型链Person.prototype === person.__proto__,结果为true
- Array.prototype === arr.__proto__,结果为true
- Object.prototype === Person.__proto__,结果为true
- Object.prototype === person.__proto__.__proto__ 沿着原型链一直向上寻找,找到Object.prototype就判断为true
除此之外,Set,Map,Sympol等新加入的ES数据类型也能用原型链instanceof的方法去判断。
手写instanceof
好了,了解清楚之后,写一个笔试常见JS
function instanceof(target, class) {
// 参数检查
if(!target || !class || !target.__proto__ || !class.prototype){
return false;
}
let current = target;
while(current) { // 一直往原型链上面找
if(current.__proto__ === class.prototype) {
return true; // 找到了返回true
}
current = current.__proto__;
}
return false; // 没找到返回false
}
function Parent() {}
function Child() {}
Child.prototype.__proto__ = Parent.prototype;
const obj = new Child();
console.log(myInstanceof(obj, Child) ); // true
console.log(myInstanceof(obj, Parent) ); // true
console.log(myInstanceof({}, Parent) ); // false
复制代码
总结
- 搞清楚原型,实例,构造函数的关系
- 理解Instanceof的原理跟用法
- 明白Object.prototype.__proto__ === null 是为了杜绝原型链死循环的设定
- 通过原型链 清晰理解子类继承父类的原理,如图所示,新建一个对象实例时,就能继承父类的属性跟方法。