JavaScript是一门高级编程语言,它是构建Web应用程序的灵魂。JavaScript的大部分都是基于对象的,它的对象机制是通过原型和原型链来实现的。因此,了解JavaScript原型和原型链非常重要。本文将详细介绍JavaScript原型和原型链。
## 1.1 JavaScript原型
在JavaScript中,一个对象可以通过其他对象进行继承。在对象继承中,每个对象都有一个隐式继承的属性称为“原型”。一个对象的原型可以是另一个对象,这个对象的原型又可以是另一个对象,以此类推形成一个链,被称为“原型链”(prototype chain)。
用代码演示一下:
var foo = {a: 1};
var bar = {b: 2};
bar.__proto__ = foo;
console.log(bar.a); // 输出1
上面的例子中,我们创建了两个对象:foo和bar。然后通过bar的__proto__属性将它的原型设置为foo对象。最后在控制台中输出了bar.a,这个值为1,因为a属性在foo对象中定义了。
在JavaScript中,所有的对象都有一个__proto__属性,该属性指向了创建该对象时所用的构造函数的原型。实际上,对象之间的关系就是通过__proto__属性建立起来的。
## 1.2 JavaScript原型链
在JavaScript中,每个实例对象(object)都必须有一个指针指向它的原型对象(prototype)。JavaScript的继承是通过原型链(prototype chain)来实现的。当访问一个对象的属性时,如果该对象本身没有该属性,那么就会去它的原型对象中查找,直到找到Object.prototype(JavaScript中所有对象的祖先)为止。
用代码演示一下:
function Person(name) {
this.name = name;
}
var p = new Person('Mike');
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
上面的代码中,我们创建了一个Person构造函数,并创建了一个实例对象p。通过控制台输出,我们可以看到:
- p.__proto__ 指向了Person.prototype,这意味着p对象继承了Person.prototype对象的属性和方法;
- Person.prototype.__proto__ 指向了Object.prototype,这意味着Person.prototype对象继承了Object.prototype对象的属性和方法;
- Object.prototype.__proto__ 指向了null,这意味着Object.prototype对象没有再向上的原型对象,它本身就代表了所有对象的祖先。
当我们给一个对象设置属性时,如果该对象本身没有该属性,那么就是在它的原型对象中创建该属性。
用代码演示一下:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
var p = new Person('Mike')
console.log(p.getName()); // Mike
console.log(p.__proto__.getName === Person.prototype.getName); // true
上面的代码中,我们在Person的原型对象(Person.prototype)上定义了一个getName方法,并将该方法添加到实例对象(p)中。然后我们在控制台中输出了p.getName()的结果,结果为'Mike';最后我们判断Person.prototype.getName和p.\__proto__.getName是否相等,结果为true,这意味着Person.prototype.getName和p.__proto__.getName指向同一个函数对象。
## 1.3 构造函数、原型和实例对象的关系
在JavaScript中,构造函数、原型和实例对象之间的关系可以用下图表示:
![](https://github.com/xiaofan9/MarkdownPhotos/raw/master/jsp-prototype-1.png)
当创建一个构造函数时,JavaScript会同时创建一个与之关联的原型对象(Person.prototype),并将该原型对象的constructor属性指向该构造函数(Person.prototype.constructor === Person)。这样,我们在实例化对象时(var p = new Person()),该对象就会自动关联到原型对象(p.__proto__ === Person.prototype)。
## 2. JavaScript原型继承方式
JavaScript的原型继承是从原型对象(prototype)进行属性和方法的继承的。原型继承是指通过构造函数的prototype对象来实现继承关系,从而使得一些对象共享某些属性和方法。
继承属性和方法可以通过两种方法来实现:
### 2.1 原型链继承
原型链继承是指利用原型链机制来实现继承关系的一种方式。在原型链继承中,每个子类的原型都继承自父类的原型,从而实现对父类的继承。
实现原型链继承的步骤如下:
- 定义父类构造函数;
- 为父类构造函数添加属性和方法;
- 定义子类构造函数;
- 在子类构造函数中调用父类构造函数;
- 将子类的原型设置为父类的实例对象。
用代码演示一下:
function Animal(name) {
this.name = name;
}
Animal.prototype.getName = function() {
return this.name;
}
function Dog(name, age) {
Animal.call(this, name);
this.age = age;
}
Dog.prototype = new Animal();
var dog1 = new Dog('Tom', 1);
console.log(dog1.getName()); // Tom
console.log(dog1.age); // 1
在上面的例子中,我们定义了一个Animal构造函数,它有一个name属性和一个getName方法。然后我们定义了一个Dog构造函数,它有一个age属性。在Dog构造函数中,我们先使用Animal.call(this, name)来调用Animal构造函数,并将Dog构造函数的this作为参数传递给Animal构造函数,从而使Dog构造函数继承了Animal构造函数的属性。接着,我们将Dog.prototype设置为一个Animal实例,这样就将Animal.prototype中的所有属性和方法都复制到Dog的原型对象上。最后我们创建了一个Dog实例dog1,并验证了dog1继承了Animal的name属性和getName方法,以及Dog自己的age属性。
使用原型链继承时,有一个非常重要的注意事项,即构造函数中不要给引用类型设置默认值,因为我们在创建子类的时候,会给子类的原型对象重写父类的原型对象,这样就导致所有子类对象共享了一个原型对象,由此可能带来很多问题。示例如下:
function Animal() {
this.names = ['Tom'];
}
function Dog(name) {
this.name = name;
}
Dog.prototype = new Animal();
var dog1 = new Dog('Jack');
var dog2 = new Dog('Lily');
dog1.names.push('Jerry');
console.log(dog1.names); // ['Tom', 'Jerry']
console.log(dog2.names); // ['Tom', 'Jerry']
在上面的代码中,我们定义了一个Animal构造函数,它有一个names属性,这个属性是一个数组,并在其中添加了一个默认值'Tom'。然后我们定义了一个Dog构造函数,并将它的原型设置为Animal的实例对象。我们用dog1和dog2分别创建了两只狗,然后向dog1的names数组中添加了一个新的元素'Jerry',然后输出了dog1和dog2的names数组,结果都为['Tom', 'Jerry']。这是因为dog1和dog2的names数组指向了它们的原型对象(Animal.prototype.names),所以它们共享了原型对象中的names属性。
有些情况下,我们需要在原型链继承中给父类构造函数添加默认值。在这种情况下,我们可以采用组合继承的方式来实现。组合继承是指通过原型链实现对父类的属性与方法的继承,通过借用构造函数技术来实现对父类实例属性的继承的一种继承方式。
### 2.2 组合继承
组合继承是将原型链继承和借用构造函数继承组合在一起的一种方式。它的特点是既可以继承父类的原型上的方法和属性,又可以通过借用构造函数的方式继承父类实例上的属性,防止子类实例共享父类实例上的属性。
实现组合继承的步骤如下:
- 定义父类构造函数;
- 为父类构造函数添加属性和方法;
- 定义子类构造函数;
- 在子类构造函数中调用父类构造函数;
- 将子类的原型设置为一个新的父类实例。
用代码演示一下:
function Animal(name) {
this.name = name;
this.names = ['Tom'];
}
Animal.prototype.getName = function() {
return this.name;
}
function Dog(name, age) {
Animal.call(this, name);
this.age = age;
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
var dog1 = new Dog('Jack', 1);
var dog2 = new Dog('Lily', 2);
dog1.names.push('Jerry');
console.log(dog1.getName()); // Jack
console.log(dog1.age); // 1
console.log(dog1.names); // ['Tom', 'Jerry']
console.log(dog2.getName()); // Lily
console.log(dog2.age); // 2
console.log(dog2.names); // ['Tom']
在上面的例子中,我们定义了Animal和Dog两个构造函数,以及它们的原型对象。其中Animal构造函数定义了一个name属性和一个getName方法,Dog构造函数在继承Animal构造函数的属性时,使用Animal.call(this, name)来调用Animal构造函数,并将Dog的this作为参数传递给Animal构造函数。在继承Animal构造函数的原型对象时,我们使用了一个新的Animal实例,将Dog.prototype设置为该实例,然后将Dog.prototype.constructor设置为Dog。这样我们实现了对父类的原型对象和实例属性的继承,在dog1和dog2实例中,我们都可以修改它们的names数组,但是它们是互相独立的,修改一个实例中的names数组不会影响其他实例中的names数组。
要注意的是,组合继承是JavaScript中最常用的继承方式之一,但是它也有一些缺点,即它可以产生两次调用父类构造函数的问题。因为在执行new Animal()的时候,Animal构造函数中的所有属性和方法都会被添加到Dog的原型对象上;而在执行Animal.call(this, name)的时候,Animal构造函数中的所有属性和方法都会被添加到Dog的实例对象上。所以我们可以使用ES6中的class语法糖来实现继承,避免这个问题的产生。
## 3. ES6中的class和继承
ES6中引入了class和继承的新语法,可以让我们更方便地实现面向对象编程的思想。class是JavaScript中的一个类,提供了定义类的方法,包括类的构造函数和类的方法。继承则是通过extends关键字和super函数来实现的。
### 3.1 定义类
用代码演示一下:
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
var p = new Person('Mike');
console.log(p.getName()); // Mike
在上面的例子中,我们使用class关键字定义了一个Person类,并在类的构造函数(constructor)中定义了一个name属性;同时在类中还定义了一个getName方法,用于获取name属性的值。然后我们通过new关键字创建了一个实例对象p,并调用了它的getName()方法,输出了'Mike'。
### 3.2 通过继承实现类的扩展
用代码演示一下:
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Dog extends Animal {
constructor(name, age) {
super(name);
this.age = age;
}
}
var dog1 = new Dog('Tom', 1);
console.log(dog1.getName()); // Tom
console.log(dog1.age); // 1
在上面的例子中,我们定义了一个Animal类和一个Dog类,其中Dog类是Animal类的子类,它继承了Animal类的所有属性和方法,并新增了一个age属性。在Dog类的构造函数中,我们使用了super关键字调用了父类(Animal)的构造函数,并将name作为参数传入。这样,Dog实例对象就有了两个属性:name和age,而且它还可以调用父类Animal中的getName方法。
## 4. 总结
本文主要介绍了JavaScript中的原型和原型链。JavaScript中的对象可以通过原型对象继承另一个对象的属性和方法,而原型链则是通过遍历每个对象的__proto__属性来实现这段继承关系的。JavaScript的原型继承有两种方式,即原型链继承和组合继承。在ES6中,我们可以通过class和继承语法来更加方便地实现面向对象的编程思想。