JavaScript深入 — 原型与原型链
- 一、原因
- 二、使用class实现继承
- 普通的类
- 实现继承
- 三、原型
- 四、原型链
- 小结
- 原型
- 原型链
- prototype和proto
- 引申


一、原因
JavaScript中除了基础类型外的数据类型,都是对象(引用类型)。但是由于其没有类(class)的概念,如何将所有对象联系起来就成了一个问题,于是就有了原型和原型链的概念。
原型链是JavaScript中实现对象属性继承的一种机制。
原型:构造函数会有一个原型对象 prototype,保存了该构造函数,以及定义的方法;而由构造函数生成的实例,则有一个私有属性 [[prototype]],也同样指向该构造函数的 prototype(在很多环境下都可以使用 __proto__ 访问,但现在已经提供了接口,我们可以通过 Object.getPrototypeOf(instance) 访问。
原型链:前面我们提到的构造函数的 prototype,也有私有属性[[prototype]] 指向另一个 prototype,最终指向 Object.prototype,该原型的__proto__ 指向 null。
class Teacher {
constructor(name){
this.name = name;
}
sayHi(){
console.log(this.name + ": hi")
}
}
const teacherA = new Teacher("A");
👆 如上代码片段解析

二、使用class实现继承
因为JS中没有类(class)这个概念,所以JS的设计者使用了构造函数来实现继承机制
ES6中的
class可以看作只是一个语法糖,它的绝大部分的功能,ES5都可以做到,新的class写法只是让原型的写法更加的清晰、更像面向对象编程的语法而已。下文也会进一步的说明。 —— 摘自阮一峰的ES6入门
普通的类
class Student {
constructor(name, score) {
this.name = name;
this.score = score;
}
introduce() {
console.log(`I am ${this.name}, I got ${this.score} in the exam.`);
}
}
const student = new Student("小明", 59);
console.log("student", student);
student.introduce();
实现继承
class Person {
constructor(name) {
this.name = name;
}
}
class Student extends Person {
constructor(name, score) {
super(name);
this.score = score;
}
}
三、原型
🌰 依然是上面的例子,当我们在控制台打印 student 时,我们发现其中并没有 introduce 这个方法

展开 [[Prototype]](或__proto__)后,我们可以看到 introduce 方法,而这个[[Prototype]] 就是 student 这个对象的隐式原型

而 Student 类上会有一个属性 prototype,通过验证可知其与student.__proto__ 指向同一地址空间,而 Student.prototype 被称为显式原型

当我们在一个对象上尝试查找一个属性或方法时,如果找不到对应的属性或方法,就会往它的隐式原型上去找

四、原型链
🌰 依然是上面的例子
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, I am ${this.name}`);
}
}
class Student extends Person{
constructor(name, score) {
super(name);
this.score = score;
}
introduce() {
console.log(`I am ${this.name}, I got ${this.score} in the exam.`);
}
}
👇 打印 student 有如下结果

👇 完整原型链如下

小结
原型
在JavaScript中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype 属性,这个属性指向函数的原型对象,使用原型对象的好处是所有对象实例共享它所包含的属性和方法
原型链
每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节
prototype和proto
prototype是构造函数的属性__proto__是每个实例都有的属性,可以访问[[prototype]]属性,当然[[prototype]]也有__proto__属性- 实例的
__proto__与其构造函数的prototype指向的是同一个对象

__proto__并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用Object.getPrototypeOf方法来获取实例对象的原型,然后再来为原型添加方法/属性。
——摘自阮一峰的ES6入门
引申
为什么JS不像C++/Java那样用类、指针这种,而是用原型和原型链?
🌰 看代码说输出:
function Person(name) {
this.name = name;
}
const person = new Person("Tom");
console.log(Person.prototype); // {}
console.log(Person.__proto__); // ƒ () { [native code] }
console.log(person.prototype); // undefined
console.log(person.__proto__); // {}
Object 和 Object.prototype:
复习时突然有点混淆上述两者了,以为 Object 是原型链终点,其实是 Object 的原型,即 Object.prototype。
Object 本身是一个构造函数,所以Object.__proto__ 其实指向的是Function.prototype,而Function.prototype.__proto__ 指向了 Object.prototype。

- 构造函数既有
prototype,指向自己的原型;又有__proto__,指向其继承的构造函数。 - 构造函数继承原型链的顶端Function,其
__proto___指向自己的prototype,如上图,使得Object和Function在原型链顶端形成了一个类似“环形”的链,第一次遇到时可能难以想明白。
这也解释了我的疑惑,为什么判断类型的方法不能用 Object.toString,而是Object.prototype.toString,二者同名但不是一个方法。
前者沿原型链找到 Function.prototype,调用其 toString 方法,而后者则是Object 原型上定义的方法。
console.log(Object.toString === Object.prototype.toString); // false
console.log(Object.toString === Function.prototype.toString); // true



















