每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法
1. 理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。
在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。
function test(){} console.log(test.prototype.constructor === test);//true
创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部 属性 __proto_ ),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫[[Prototype]]。虽然在脚本中 没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性 __proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就 是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
如下:
function test(){} test.prototype.name = '123'; var t1 = new test(); console.log(t1.__proto__ === test.prototype)
图上 展示了 Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系。 在此,Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person。 原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。Person 的每个实例—— person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype;换句话说,它们 与构造函数没有直接的关系。
要格外注意的是,虽然这两个实例都不包含属性和方法,但却可以调用原型对象上的属性和方法 ,例如 person1.sayName() 这是通过查找对象属性的过程来实现的。
可以通过以下2个方法判断,t1是否指向了 test.prototype :
isPrototypeOf() 可以用来测试 t1是否指向了 test.prototype
Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]
属性的值)。
function test(){} test.prototype.name = '123'; var t1 = new test(); console.log(test.prototype.isPrototypeOf(t1)); //true console.log(Object.getPrototypeOf(t1) === test.prototype); //true
在以上例子中,t1 没有name属性,但是它会执行2次搜索,先搜索实例本身,是否有该属性,如果没有,则会搜索它的原型,如果则调用。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们 在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,
该 属性将会屏蔽原型中的那个属性。如下:
function test(){} test.prototype.name = '123'; var t1 = new test(); t1.name = 'zs'; console.log(t1.name); //'zs'
在上面的例子中,t1 的name 被一个新值,屏蔽了,在读取它时,在搜索时,实例本身上有该属性,就不会再去查找原型,即使将这个属性设置为 null,也 只会在实例中设置这个属性,而不会恢复其指向原型的连接。
不过,使用 delete 操作符则可以完全删 除实例属性,从而让我们能够重新访问原型中的属性。
function test(){} test.prototype.name = '123'; var t1 = new test(); t1.name = 'zs'; console.log(t1.name); //'zs' delete t1.name; console.log(t1.name); //'123'
如果需要判断使用属性是来自实例本身属性还是原型中的,可以使用 hasOwnProperty() 方法判断,
hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不 要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。
function test(){} test.prototype.name = '123'; var t1 = new test(); t1.age = 23; console.log(t1.hasOwnProperty('name')); //false 原型上的属性 console.log(t1.hasOwnProperty('age')); //true 实例本身的属性
2. 原型与 in 操作符
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
function test(){} test.prototype.name = '123'; var t1 = new test(); t1.age = 23; //来自原型 console.log('name' in t1); //true console.log(t1.hasOwnProperty('name'));//false // 来自实例本身 console.log('age' in t1); //true console.log(t1.hasOwnProperty('age'));//true
在以上代码执行的整个过程中,name 属性要么是直接在对象上访问到的,要么是通过原型访问到 的。因此,调用"name" in t1 始终都返回 true,
同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于 原型中。
function test(){} test.prototype.name = '123'; var t1 = new test(); t1.age = 23; console.log( 'name' in t1 && t1.hasOwnProperty('name')); // false --来自原型 console.log( 'age' in t1 && t1.hasOwnProperty('age')); // true --来自实例
function hasPrototypeProperty(object, name){ return object.hasOwnProperty(name) && (name in object); }
在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中 既包括存在于实例中的属性,也包括存在于原型中的属性。
屏蔽了原型中不可枚举属性 (即将 [[Enumerable]]标记为 false 的属性) 的实例属性也会在 for-in 循环中返回,因为根据规定,所 有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。
要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function test(){} test.prototype.name = '123'; test.prototype.job = '测试'; var t1 = new test(); t1.age = 21; console.log(Object.keys(test.prototype)); // name,job console.log(Object.keys(t1)); // age
所得的结果都是保存在一个数组中,如原型对象中 可枚举的属性 是 name、job,这 个顺序也是它们在 for-in 循环中出现的顺序。
如果是通过 t1 的实例调用,则 Object.keys() 返回的数组只包含"age"这个实例属性。
如果你想要得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames() 方法。
function test(){} test.prototype.name = '123'; test.prototype.job = '测试'; var t1 = new test(); t1.age = 21; console.log(Object.getOwnPropertyNames(test.prototype)); // constructor, name, job console.log(Object.getOwnPropertyNames(t1)); // age
注意结果中包含了不可枚举的 constructor 属性。Object.keys()和 Object.getOwnProperty- Names()方法都可以用来替代 for-in 循环。支持这两个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。
3. 更简单的原型语法
function test(){} test.prototype = { name : '123', job: '测试' }
在上面的代码中,我们将 test.prototype 设置为等于一个以对象字面量形式创建的新对象。 最终结果相同,但有一个例外:constructor 属性不再指向 test 了。
每创建一 个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在 这里使用的语法,本质上完全重写了默认的 prototype 对象,
因此 constructor 属性也就变成了新 对象的 constructor 属性(指向 Object 构造函数),不再指向 test 函数。此时,尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了,如下所示。
(1)对比1
function test(){} test.prototype.name = '123'; var t1 = new test(); console.log(t1.constructor === test); // true console.log(test.prototype);
(2)对比2
function test(){} test.prototype = { name : '123', job: '测试' } var t1 = new test(); console.log(t1 instanceof test); //true console.log(t1.constructor === test); // false console.log(test.prototype);
在(2)中的构造函数变成了Object() 了
在此,用 instanceof 操作符测试 test 仍然返回 true,但 constructor 属性则 等于 Object 而不等于 test 了。如果 constructor 的值真的很重要,可以像下面这样特意将它设 置回适当的值。
function test(){} test.prototype = { constructor : test, // 这里 name : '123', job: '测试' } var t1 = new test(); console.log(t1 instanceof test); //true console.log(t1.constructor === test); // false console.log(test.prototype);
注意,以这种方式重设 constructor 属性会导致它的[[Enumerable]]( 枚举属性 )特性被设置为 true。默认 情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引 擎,可以试一试 Object.defineProperty()。
function test(){} test.prototype = { constructor : test, name : '123', job: '测试' } //重设构造函数,只适用于 ECMAScript 5 兼容的浏览器 Object.defineProperty(test.prototype, "constructor", { enumerable: false, value: test });
4. 原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上 反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。
function test(){} test.prototype.name = 'zs'; var t1 = new test(); test.prototype.sayName = function(){ console.log(this.name); } t1.sayName(); //zs
以上代码先创建了实例,然后新增了 sayName() 方法, 但是仍然可以访问到这个新方法,其原因可以归结为实例与原型之间的松散连接关系。
当我们调用 t1.sayName() 时,首先会在实例中搜索名为 sayName 的属性,在没找到的情况下,会继续搜索原型。因为实例与原型 之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayName 属性并返回保存 在那里的函数。
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重 写整个原型对象,那么情况就不一样了。
我们知道,调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。 请记住: 实例中的指针仅指向原型,而不指向构造函数。
function test(){} test.prototype.name = 'zs'; var t1 = new test(); test.prototype = { constructor: test, sayName : function(){ console.log(this.name); } } console.log(t1.name); // 'zs' t1.sayName(); //error t1.sayName is not a function
5. 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式 创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。 9 例如,在 Array.prototype 中可以找到 sort()方法,而在 String.prototype 中可以找到 substring()方法,如下所示。
console.log(Array.prototype);
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自 定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。
Array.prototype.t1 = function(){ return [1,2,3]; } var arr = new Array(); console.log(arr.t1()); //[1,2,3]
6. 原型对象的问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在 默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。 原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒 也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属 性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子。
function test(){}; test.prototype.arr = [1,2,3]; var t1 = new test(); var t2 = new test(); t1.arr.push(5); console.log(t1.arr); // 1,2,3,4,5 console.log(t2.arr); // 1,2,3,4,5
7 组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。
function test(name, age, arr){ this.name = name; this.age = age; this.arr = arr; }; test.prototype.sayName = function(){ console.log(this.name); }; var t1 = new test('zs', 21, [1,2,3]); var t2 = new test('ls', 22, [3,2,1] ); t1.arr.push(5); console.log(t1.arr); // 1,2,3,5 console.log(t2.arr); // 1,2,3 t1.sayName(); // zs t2.sayName(); //ls