JS 里有一个很重要的概念就是原型,不管在日常工作中还是面试中经常会遇到,下面我就来系统地给大家讲解下 JS 里原型的相关知识。
1.是什么
原型是每个函数都有的一个属性,是一个指针,指向一个对象,对象里包含一些属性和方法,由所有实例共享,当然这些属性和方法是根据具体业务情况自己添加的。
2.用处
由1可知,原型存在的目的就是为了让所有实例共享属性和方法,以便节约内存、共享数据。
3.示例
// 定义函数 Person
function Person(){}
// 在函数 Person 的原型上添加共享的属性和方法
Person.prototype.name = "Nicholas";
Person.prototype.sayHello = function(){
alert("Hello World!");
};
// 定义实例 person1 并调用其 sayName 方法
var person1 = new Person();
console.log(person1.name) // "Nicholas"
person1.sayHello (); // "Hello World!"
// 定义实例 person2 并调用其 sayName 方法
var person2 = new Person();
console.log(person2.name) // "Nicholas"
person2.sayHello (); // "Hello World!"
// 以上两个实例都能取到原型上定义的 name 属性,也都能调用原型上定义的 sayName 方法,实现了属性和方法的共享
4.原型对象的理解
1.函数的 prototype 属性指向原型对象
2.原型对象的 constructor 属性指回函数
3.实例的 [[Propotype]] 属性指向原型对象(某些浏览器可用 __proto__ 访问该属性,其他实现不可访问)
判断实例和原型对象的关系有两个方法:isPrototypeOf() 和 Object.getPrototypeOf()
- isPrototypeOf() 方法:确定是否是实例的原型对象
alert(Person.prototype.isPrototypeOf(person1)); // true
- Object.getPrototypeOf() 方法:返回 [[Propotype]] 的值
alert(Object.getPrototypeOf(person1) == Person.prototype); // true
alert(Object.getPrototypeOf(person1).name); // "Nicholas"
4.读取对象属性时,先检查实例本身是否有该属性,没有则检查其原型对象是否有该属性,一旦设置了实例的属性,则只有 delete 后才能访问到原型的相同属性
检查属性是否存在有两种方法:hasOwnProperty() 和 in
- hasOwnProperty() 方法:检查一个属性是否存在于实例中
function Person(){}
Person.prototype.name = "Nicholas";
var person1 = new Person();
alert(person1.hasOwnProperty("name")); // false
- in:检查一个属性是否存在,无论实例或原型中
function Person(){}
Person.prototype.name = "Nicholas";
var person1 = new Person();
alert("name" in person1); // true
5.遍历属性
遍历对象属性有 3 种方法:for-in、Object.keys() 和 Object.getOwnPropertyNames()
- for-in:遍历所有可访问、可枚举属性名(即使设置了 [[Enumerable]] 为 false 的属性也会被返回,因为根据规定所有开发人员定义的属性都是可枚举的,IE8 及更早版本除外)
function Person(){}
Person.prototype.name = "Nicholas";
var person1 = new Person();
for (var prop in person1){
alert(prop); // “name”
}
- Object.keys() 方法:获取所有可枚举的实例属性名
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); // "name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); // "name,age"
- Object.getOwnPropertyNames() 方法:所有实例属性,无论是否可枚举
// 接上示例
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // "constructor,name,age,job,sayName"
// constructor 这种不可枚举属性可会被返回
6.简单原型语法
以上写原型的语法太累赘,可以用一个包含所有属性和方法的对象字面量来重写整个原型对象
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
注意:此时 constructor 属性不再指向 Person,而是指向 Object 构造函数,通过 constructor 已经无法确定对象的类型了
var friend = new Person();
alert(friend instanceof Object); // true
alert(friend instanceof Person); // true
alert(friend.constructor == Person); // false
alert(friend.constructor == Object); // true
解决办法:可设置回 Person
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
这种方式会导致 constructor 属性的 [[Enumerable]] 特性被设置为 true(即可枚举),可通过 Object.defineProperty() 的方式设置回来
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
7.原型动态性
原型动态性体现在:先创建实例,再修改原型,也会反映到实例上
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); // "hi"
但注意:先创建实例,再重写原型,则不会反映到实例上,因为已存在的实例的原型仍是最初的原型
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); // error
8.原生对象原型
所有原生引用类型(Object、Array、String 等)都在其构造函数的原型上定义了方法。 例如,在 Array.prototype 中可找到 sort() 方法,在 String.prototype 中可找到 substring() 方法。
可以像修改自定义对象的原型一样修改原生对象的原型,所以可获取所有默认方法的引用,也可添加新方法。
// 给基本类型 String 添加一个名为 startsWith() 的方法:
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); // true
注意:这种方式不推荐,因为一是可能会导致命名冲突,二是可能意外地重写原生方法。
9.缺点
使用原型模式构造对象的缺点在于:原型上定义的属性会被实例共享,对于函数类型的属性而言很有用,对于基本值类型的属性也还好(实例上添加一个同名属性就会覆盖原型上的属性),但对于引用类型的属性,一个实例改了也会反映到另一个实例上,这是我们不想要的。
function Person(){}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName: function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); // "Shelby,Court,Van"
alert(person2.friends); // "Shelby,Court,Van"
alert(person1.friends === person2.friends); // true
解决办法是:组合使用原型模式和构造函数模式,原型模式用于定义方法和共享属性,构造函数模式用于定义实例属性(最大限度地节省了内存,且支持向构造函数传递参数)
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); // "Shelby,Count,Van"
alert(person2.friends); // "Shelby,Count"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true
此模式是目前使用最广泛、认同度最高的一种创建自定义类型的方法,可以说这是定义引用类型的一种默认模式。