js面向对象的程序设计 --- 中篇(创建对象) 之 原型模式
·原型模式
我们创建的每一个函数都由一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有
实例共享的属性和方法。
如果按照字面意思来理解,那么prototype就是通过构造函数创建的那个对象实例的原型对象。使用原型对象的好处就是可以让所有对象实例共享它
所包含的属性和方法
// code示例
function Person(){} Person.prototype.name = 'Nicholas'; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { console.log(this.name); }; var person1 = new Person(); person1.sayName(); //Nicholas var person2 = new Person(); person2.sayName(); //Nicholas console.log(person1.sayName === person2.sayName); // true
理解原型对象:
1:无论什么时候,只要创建了一个新函数们就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
2:在默认情况下,所有原型对象都会自动获得一个constructor属性,至于其它方法,则都是从Object继承来的。
3:当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMAScript中管这个
指针叫[[Prototype]]
4: 要明确 [[Prototype]] 这个连接存在于实例与原型对象之间,而不是存在于实例与构造函数之间
示意图:
// 图片
对示意图的理解:
1:Person.prototype指向了原型对象。而Person.prototype.constructor又指回了Person
2:Person的每一个实例,都包含一个内部属性,该属性仅仅指向了Person.prototype
3:原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问
4:当代码读取某个对象的属性时,都会执行一次搜索,如果在实例中找到,则返回该属性。没有则在原型对象中查找
5:当为对象实例添加一个属性时,这个属性会屏蔽原型对象的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改
那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。
注意:所有实现都无法访问到[[Prototype]],那我们怎么得到一个对象的原型对象呢?
// 返回[[Prototype]]的值 console.log(Object.getPrototypeOf(person1) === Person.prototype); // true 使用Object.getPrototypeOf()返回的对象其实就是这个对象的原型,而这在利用原型实现继承的情况下是非常重要的
注意:我们如何得出一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true
function Dog(){ } Dog.prototype.name = "tom"; var dog_a = new Dog(); console.log(dog_a.hasOwnProperty("name")); // false dog_a.name = "jack"; // 该实例重写了name属性 console.log(dog_a.hasOwnProperty("name")); // true
原型与in操作符
有两种情况使用in操作符:单独使用和在for-in循环使用
·单独使用:
in操作符通常会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
于是我们可以结合对象的hasOwnProperty()方法设计一个公共函数,来确定是否只存在原型中
// code..
// 如果属性只存在原型中则返回true function test(obj,val){ return (!obj.hasOwnProperty(val)) && val in obj; }
·for-in循环:
使用for-in循环,返回的时所有能够通过对象访问,可枚举(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性
更简单的原型语法
为了减少不必要的输入,也为了从视觉上更好的封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象
//code..
function Person(age){ this.age = age; } Person.prototype = { area : "CHINA", say : function () { console.log('hello'); } } var p1 = new Person(11); console.log(p1.constructor); //ƒ Object() { [native code] } // 这里出了一个意外,constructor属性不再指向Person了。我们刚才的语法,实际上是重写了默认的prototype对象,因此constructor属性 //变成了新对象的constructor属性(指向Object)构造函数,尽管instanceof操作符还能返回正确结果,但通过constructor已无法确定对象类型 console.log(p1 instanceof Person); console.log(p1.constructor == Person); console.log(p1.constructor == Object); // 解决方式初级版 Person.prototype = { constructor : Person, area : "CHINA", say : function () { console.log('hello'); } } //弊端:这种方式重设constructor属性会导致它的[[Enumerable]]特性为true。默认情况下,原生的constructor属性是不可枚举的。 //最终解决方法 Object.defineProperty(Person.prototype,"constructor",{ enumerable : false, value : Person })
原型的动态性
由于在原型中查找值的过程中是一次搜索,因此我们对原型对象所作的任何修改都能够立即从实例上反应出来---即使是先创建了实例后修改原型也照样如此。
function Person (name) { this.name = name; } Person.prototype.area = 'china'; var friend = new Person('cl'); console.log(friend.area); // china Person.prototype.area = 'japan'; console.log(friend.area); // japan // 实例与原型对象之间的连接只不过是一个指针,而非一个副本 Person.prototype = { area : 'test', } console.log(friend.area); // japan // 重写原型对象切断了现有原型与之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有远程的引用类型,都是采用这种模式创建的。
所有原生引用类型(Object,Array,String等等),都在其构造函数的原型上定义了方法。
console.log(typeof Array.prototype.sort); // function console.log(typeof String.prototype.substring); // function // 通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型 // 因此可以随时添加方法 String.prototype.startsWith = function () { return this.indexOf(text) == 0; } // 尽管可以这样做,但是不推荐。因为这样会导致命名冲突或者意外的重写原生方法
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数与原型模式。构造函数模式用来定义实例属性,而原型属性用与定义方法和共享的属性。
结果,每个实例都会有自己的一份实例属性的副本,但又共享着对方法的引用,最大限度地节省了内存
这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认方式
动态原型模式
有其他oo语言经验的开发人员看到独立的函数和原型时,很可能会感到困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在
构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持同时使用构造函数和原型的优点。
function Person(name ,age, job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != "function"){ // 这段代码只有在初次调用构造函数才会执行 Person.prototype.sayName = function () { console.log(this.name); } } } // 使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系
寄生构造函数模式
function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { console.log(this.name); } return o; } var friend = new Person('Ni',29,"eng"); friend.sayName(); // Ni //在这个例子中,Person函数创建了一个对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用new操作符并且把使用的包装函数 // 叫做构造函数之外,这个模式跟工厂模式其实是一模一样的
稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也没不引用this对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),
或者在方式数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的常例方法不引用this;
二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下:
function Person(name){ var o = new Object(); o.sayName = function () { console.log(name); } return o; } var friend = Person('Ni'); friend.sayName(); // Ni //注意 : 在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他方法访问name的值 // 即使有其他代码会给这个对象添加方法和数据成员,但也不可能有别的方法传入构造函数中的初始数据。 // 构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境。