JS作为一个主打函数式编程范式的语言,对面向对象的实现相比其他语言有点特别。
在ES6引入class语法糖前,JS的类是这么写的:
// 构造函数
function Person(name) {
// 类变量
this.name = name;
}
// 类方法
Person.prototype.hi = function() {
console.log('Hi, I am ' + this.name + '.');
}
const person = new Person('Sam');
person.hi(); // Hi, I am Sam.
类变量直接通过this.xxx申明在构造函数里,类方法则需要放在prototype也就是原型上。
为什么类方法要放在原型上?我直接跟类变量一样申明不香吗?比如:
function Person(name) {
this.name = name;
this.hi = function() {
console.log('Hi, I am ' + this.name + '.');
}
}
const person = new Person('Sam');
person.hi(); // Hi, I am Sam.
确实可以,但是这样会导致同一个类方法被拷贝多次,比如:
const person = new Person('Sam');
const anotherPerson = new Person('Mary');
console.log(person.hi === anotherPerson.hi);
上面这段代码,prototype写法下返回true,而直接申明在构造函数里的写法返回false,也就是说直接申明在构造函数里的写法会导致每次new都会将同一个方法拷贝一次,非常浪费内存。
所以类方法一定要定义在原型prototype上。
原型不仅可以防止方法重复拷贝,还是JS继承的实现方式。
定义一个Student类来继承之前的Person类:
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype, {
constructor: {
value: Student,
enumerable: false,
writable: true,
configurable: true,
},
});
Student.prototype.hello = function() {
console.log('Hello, I am ' + this.name + ', a grade ' + this.grade + ' student.');
}
const student = new Student('Mary', 3);
student.hi(); // Hi, I am Mary.
student.hello(); // Hello, I am Mary, a grade 3 student.
关键的继承代码有两块,一个是Student构造函数内需要调用Person的构造函数来保证Person的类变量被Student继承:
Person.call(this, name);
另一个是令Student的原型为Person的空对象:
Student.prototype = Object.create(Person.prototype);
但是只靠上面的代码会导致Student.prototype.constructor === Person,所以我们还要把Student原型的constructor字段指向Student构造函数:
Student.prototype = Object.create(Person.prototype, {
constructor: {
value: Student,
enumerable: false,
writable: true,
configurable: true,
},
});
其中enumerable、writable、configurable分别代表getOwnPropertyDescriptor会返回的property descriptor中的可枚举(可以for...in或Object.keys())、可修改、可删除(delete字段)。
为什么Student的原型是Person的对象?为什么要把Student.prototype.constructor指向Student?
这就要说到原型链的结构。原型链其实就是爷类->父类->类->子类->孙类组成的一条类的继承链,在我们的例子中就是Object->Person->Student(Object是JS所有对象的默认父类),链的每一个环节由三个部分构成:构造函数(Person)、原型(Person.prototype)、对象(person):

三个指向:
构造函数.prototype = 原型
原型.constructor = 构造函数
对象.__proto__ = 原型
这就是为什么上文代码中令Student.prototype.constructor = Student
两种初始化对象的方式:
对象 = new 构造函数()
对象 = Object.create(原型)
可以试着跑一下代码验证一下:
console.log(Person.prototype.constructor === Person); // true
console.log(person.__proto__ === Person.prototype); // true
原型链的每两个相邻环节之间,子类的原型是父类的对象,也就是说Person.prototype是Object的对象,Student.prototype是Person的对象。
可以试着跑一下代码验证一下:
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true
把Object->Person->Student三个环节画在一张图里:

图中的Student.prototype--.__proto-->Person.prototype--.__proto-->Object.prototype--.__proto-->null这一条就被叫做原型链
这就是为什么上文代码中令Student.prototype为Person的空对象
除了继承,原型链还跟JS执行函数的机制相关,比如:
当执行student.hi()的时候,JS会按以下顺序寻找hi函数的定义:
先在Object.getOwnPropertyNames(student)上找是否是构造函数中通过this.hi定义的
再在Student.prototype上找Student.prototype.hi
再在Student.prototype.__proto__也就是Person.prototype上找Person.prototype.hi(找到)
再在Person.prototype.__proto__也就是Object.prototype上找Object.prototype.hi
最后Object.prototype.__proto__为null,到达原型链终点,查找结束
instanceof关键字也是相似的原理,student instanceof Person之所以为true就是通过原型链一步一步找到了Person.prototype
差不多就写这么多~面试的话理解并记忆上面的原型链图就够了~