您的位置:

原型与原型链

一、什么是原型

在JavaScript中,每个对象都有一个指向另一个对象的引用,叫做原型。原型是JavaScript中一个比较重要的概念。

通过使用构造函数创建的对象,会自动拥有一个原型对象。原型的作用就是用来继承属性和方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log("Hello, " + this.name);
};

var person1 = new Person("Alice", 18);
var person2 = new Person("Bob", 20);

person1.sayHello(); // "Hello, Alice"
person2.sayHello(); // "Hello, Bob"

在这个例子中,我们定义了一个Person对象,通过Person的原型来增加了一个sayHello方法,然后我们根据这个构造函数来创建两个不同的对象person1和person2,通过调用sayHello方法,我们可以看到两个对象都可以正确的继承到sayHello方法。

二、原型链的作用

原型是可以被继承的,由此形成的继承关系被称为原型链。当对象调用方法或属性时,如果本身找不到,就会去原型对象中寻找,如果还是找不到,则会继续去原型对象的原型对象中查找,构成一个链式结构,直到最后查找完整个链才会返回undefined。

function Animal() {
  this.name = "Animal";
}

Animal.prototype.eat = function() {
  console.log(this.name + " is eating");
};

function Cat() {
  this.name = "Cat";
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat1 = new Cat();
cat1.eat(); // "Cat is eating"

在这个例子中,我们定义了一个Animal构造函数,然后在它的原型上添加一个eat方法。然后我们又定义了一个Cat构造函数,由于我们希望Cat继承Animal的属性和方法,所以我们将Cat的原型指向了Animal的实例,这样一来Cat就可以调用到Animal的属性和方法了。

当我们调用cat1的eat方法时,首先在cat1对象上寻找是否有eat属性或方法,没有找到,于是它会去cat1的原型对象Cat.prototype中查找,还是没有找到,于是它又去Cat.prototype的原型对象Animal.prototype中查找,最终在Animal.prototype中找到了eat方法,然后执行。这就是原型链的查找过程。

三、原型链的细节

在JavaScript中,有些方法是自身属性,有些是继承属性;也有一些属性是自身的,有一些是继承来的。由于原型链的继承关系,有些情况下会出现一些意料之外的结果,下面举几个例子说明一下:

var str = "hello";
console.log(str.toString()); // "hello"

var arr = [1,2,3];
console.log(arr.toString()); // "1,2,3"

在这两个例子中,我们在字符串和数组对象上调用了toString方法,然而这两个对象并没有自己的toString方法,是从它们的构造函数Object中继承了toString方法。这也是为什么其他对象也可以使用toString方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log("Hello, " + this.name);
};

var person1 = new Person("Alice", 18);
var person2 = new Person("Bob", 20);

console.log("name" in person1); // true
console.log(person1.hasOwnProperty("name")); // true
console.log(person1.hasOwnProperty("sayHello")); // false 

console.log("sayHello" in person1); // true
console.log(person1.__proto__.hasOwnProperty("sayHello")); // true
console.log(person1.__proto__.hasOwnProperty("name")); // false

在这个例子中,我们通过person1对象来演示hasOwnProperty方法和in方法的区别。hasOwnProperty是检测对象自身是否拥有某个属性,而in方法是检测对象是否拥有某个属性,不管是自身还是继承而来的。因为person1对象自己拥有name属性,所以hasOwnProperty返回true,而因为sayHello方法是从它的原型对象Person.prototype上继承而来的,所以hasOwnProperty返回false,in方法却返回true。

四、如何组合使用原型与构造函数

在实际编程中,常常需要使用原型和构造函数来一起工作。比如,希望继承某个对象,并在继承的同时传递一些参数;或者希望封装私有变量,但又能让实例对象共享某些公共的属性和方法等等。以下给出一些例子:

// 1、通过原型继承属性和方法,通过构造函数来传递参数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log("Hello, " + this.name);
};

function Student(name, age, school) {
  Person.call(this, name, age);
  this.school = school;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

Student.prototype.sayHello = function() {
  console.log("Hello, I'm a student, my name is " + this.name);
};

var student1 = new Student("Alice", 18, "Harvard");
student1.sayHello(); // "Hello, I'm a student, my name is Alice"

// 2、使用原型来封装私有变量
function Counter() {
  var count = 0;
  this.getCount = function() {
    return count;
  };
}

Counter.prototype.increment = function() {
  var count = this.getCount();
  count++;
  this.getCount = function() {
    return count;
  };
};

var counter1 = new Counter();
counter1.increment();
console.log(counter1.getCount()); // 1

var counter2 = new Counter();
counter2.increment();
console.log(counter2.getCount()); // 1,而不是2

以上这两个例子都非常常见。第一个例子通过Student构造函数来传递参数,同时继承了Person的属性和方法;第二个例子封装了私有变量,并通过原型来共享increment方法。

五、总结

原型和原型链是JavaScript中比较重要的概念,通过灵活运用原型和构造函数,我们可以很方便地实现继承、私有变量等功能。同时,需要注意的是,由于原型链的存在,有些方法可能并不是对象自身的,而是继承而来的,因此在编程时需要特别注意。