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