从一个问题开始
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name;
};
const alice = new Person('Alice');
const bob = new Person('Bob');
console.log(alice.sayHello === bob.sayHello); // true
console.log(alice.__proto__ === Person.prototype); // true
为什么 alice 和 bob 能共享 sayHello 方法?为什么 alice.__proto__ 等于 Person.prototype?
答案在原型链。
原型是什么
JavaScript 的对象有一个内部属性 [[Prototype]],在浏览器环境里暴露为 __proto__。这个属性指向另一个对象,这就是原型。
当访问对象的某个属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找,直到找到或者到达原型链的末端(null)。
const parent = { name: 'parent' };
const child = Object.create(parent);
child.age = 10;
console.log(child.name); // 'parent',从 parent 原型上找到
console.log(child.age); // 10,自己的属性
console.log(child.toString()); // 从 Object.prototype 找到
Object.create(parent) 创建一个新对象,它的原型指向 parent。
构造函数和原型
在 JavaScript 里,构造函数(用 new 调用的函数)有一个 prototype 属性,指向一个对象。当用 new 创建实例时,实例的 __proto__ 会指向构造函数的 prototype。
function Foo(name) {
this.name = name;
}
Foo.prototype.greet = function() {
return 'Hello, ' + this.name;
};
const foo = new Foo('foo');
// 关键关系:
foo.__proto__ === Foo.prototype // true
Foo.prototype.constructor === Foo // true
Foo.prototype 是一个普通对象,它有一个 constructor 属性指向 Foo。foo 实例通过 __proto__ 指向 Foo.prototype,所以能访问 Foo.prototype.greet。
这就是为什么所有实例能共享方法:方法定义在构造函数的 prototype 上,所有实例共享同一个原型对象。
原型链是什么
原型链是多个原型对象通过 __proto__ 连接形成的链式结构。
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return '...';
};
function Dog(name) {
Animal.call(this, name);
}
// 设置原型链:Dog.prototype.__proto__ = Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
return 'Woof!';
};
const dog = new Dog('Rex');
console.log(dog.name); // 'Rex',自己的属性
console.log(dog.bark()); // 'Woof!',Dog.prototype 上的方法
console.log(dog.speak()); // '...',Animal.prototype 上的方法
console.log(dog.toString()); // Object.prototype 上的方法
dog 的原型链是:dog → Dog.prototype → Animal.prototype → Object.prototype → null。
当访问 dog.speak() 时,JavaScript 引擎先在 dog 上找,没找到;然后在 dog.__proto__(也就是 Dog.prototype)上找,没找到;然后在 Dog.prototype.__proto__(也就是 Animal.prototype)上找到了。
new 操作符到底做了什么
function Person(name) {
this.name = name;
}
const person = new Person('Tom');
new Person('Tom') 做了以下事情:
- 创建一个新对象:
{} - 设置新对象的原型:
新对象.__proto__ = Person.prototype - 执行构造函数:
Person.call(新对象, 'Tom') - 返回新对象(如果构造函数没有显式返回一个对象)
关键点是:构造函数的 this 被绑定到新创建的对象,所以 this.name = name 会给新对象添加属性。
如果构造函数显式返回了一个对象,那个对象会代替默认返回的新对象。如果返回的不是对象,new 会忽略返回值,仍然返回新对象。
ES6 class 是什么
ES6 的 class 语法是原型继承的语法糖。它没有引入新的继承模型,只是让原型继承的写法更清晰。
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return 'Hello, I am ' + this.name;
}
static createAnonymous() {
return new Person('Anonymous');
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
introduce() {
return 'I am ' + this.name + ', grade ' + this.grade;
}
}
const student = new Student('Alice', 'A');
console.log(student.sayHello()); // 'Hello, I am Alice'
console.log(student.introduce()); // 'I am Alice, grade A'
console.log(Student.createAnonymous()); // 静态方法的调用
class 语法的特点:
constructor是构造函数- 方法定义在
ClassName.prototype上 static方法定义在类本身而不是原型上extends设置原型链:Student.prototype.__proto__ = Person.prototypesuper()调用父类构造函数
实际上,class 的实现和原型继承完全一致,只是语法更简洁:
// 上面的 class 等价于:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return 'Hello, I am ' + this.name;
};
Person.createAnonymous = function() {
return new Person('Anonymous');
};
原型链的末端
所有对象的原型链最终都会到达 Object.prototype,它的原型是 null。
console.log(Object.prototype.__proto__); // null
Object.prototype 上有一些重要的方法,如 toString、valueOf、hasOwnProperty。
hasOwnProperty 用于检查一个属性是不是对象自己的(而不是继承来的):
const obj = { own: true };
console.log(obj.hasOwnProperty('own')); // true
console.log(obj.hasOwnProperty('toString')); // false,继承来的
console.log(Object.prototype.hasOwnProperty.call(obj, 'toString')); // 检查继承属性的正确方式
原型链和属性查找
属性查找沿着原型链向上,时间复杂度是 O(n)。但这个 n 通常很小,不是性能问题。
属性遮蔽(Property Shadowing)发生在对象有自己的属性和原型链上同名的属性时:
const parent = { value: 'parent' };
const child = Object.create(parent);
child.value = 'child';
console.log(child.value); // 'child',自己的属性遮蔽了原型上的
console.log(parent.value); // 'parent',不受影响
删除属性时,只删除对象自己的属性,不会影响原型上的:
delete child.value;
console.log(child.value); // 'parent',现在访问到原型上的了
这一章想说的
原型链是 JavaScript 实现继承的核心机制。每个对象都有 __proto__ 指向一个原型对象,形成链式查找结构。当访问属性时,如果对象本身没有,会沿着原型链向上找。
构造函数通过 prototype 属性定义实例能共享的方法,new 操作符把实例的原型连接到构造函数的 prototype 上。
ES6 的 class 语法是原型继承的语法糖,底层实现完全一致。class 的 extends 设置原型链,super 调用父类构造函数,static 定义类级别的方法。