时间:2021-07-18 08:25:37 | 栏目:JavaScript代码 | 点击:次
通过Object构造函数或对象字面量创建对象时,使用同一个接口创建很多对象时,会产生大量的重复代码。为了简化,引入了工厂模式。
工厂模式
function createPerson(name, age, job) { var obj = new Object(); obj.name = name; obj.age = age; obj.job = job; obj.sayHello(){ alert(this.name); }; return obj; } var p1 = createPerson("xxyh", 19, "programmer"); var p2 = createPerson("zhangsan", 18, "student");
这种创建对象的方式大大简化了代码,然而也存在不足,那就是无法确定对象的类型。为了解决这个问题,出现下面这种模式。
构造函数模式
创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { alert(this.name); }; } var p1 = new Person("xxyh", 19, "programmer"); var p2 = new Person("Jack", 18, "student");
上例中,Person()取代了createPerson(),除此之外,还有几点不同:
•没有显示地创建对象;
•直接将属性和方法赋值给了this对象
•没有return语句
创建Person对象,必须使用new操作符。分为4个步骤:
•创建一个新对象
•将构造函数的作用域赋给新对象
•执行构造函数中的代码
•返回新对象
p1和p2分别保存着Person的一个实例。
alert(p1.constructor == Person); // true alert(p2.constructor == Person); // true
检测类型时最好使用instanceof:
alert(p1 instanceof Object); // true alert(p1 instanceof Person); // true alert(p2 instanceof Object); // true alert(p2 instanceof Person); // true
p1和p2都是Object的实例,因为所有对象均继承自Object。
2.1将构造函数当作函数
// 当作构造函数使用 var person = new Person("xxyh", 19, "programmer"); person.sayName(); // "xxyh" // 当作普通函数 Person("zhangsan", 18, "student"); // 添加到window window.sayName(); // "zhangsan" // 在另一个对象的作用域中调用 var obj = new Object(); Person.call(obj, "Jack", 29, "manager"); obj.sayName(); // "Jack",obj拥有了所有属性和方法
2.2构造函数的问题
使用构造函数的问题,就是每个方法都要在每个实例上重新创建一遍。p1和p2都有一个sayName()方法,但是他们不是一个Function的实例。在JavaScript中,函数时对象,因此每定义一个函数,就实例化了一个对象。
构造函数也可以这样定义:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = new Function("alert(this.name)"); }
因此,不同实例上的同名函数时不相等的:
alert(p1.sayName == p2.sayName); // false
然而,创建两个同样功能的Function是多余的,根本不需要在执行代码前就把函数绑定到特定对象上面。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName() { alert(this.name); } var p1 = new Person("xxyh", 19, "programmer"); var p2 = new Person("Jack", 18, "student");
上面将sayName()的定义移到构造函数外部,然后在构造函数内部将属性sayName设置为全局的sayName函数。这样,sayName包含了指向函数的指针,p1和p2共享了全局作用域中定义的同一个sayName()函数。
但是,这样做又出现了新问题:在全局作用域中定义的函数只能被某个对象调用。而且如果对象定义了很多方法,那么引用类型就失去了封装性。
原型链模式
每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。这个对象的用途是:包含可以由特定类型的所有实例共享的属性和方法。prototype是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。这就是说不必在构造函数中定义对象实例的信息,而是将这些信息添加到原型对象中。
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = 19; Person.prototype.job = "programmer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); person1.sayName(); // "xxyh" var person2 = new Person(); person2.sayName(); // "xxyh" alert(person1.sayName == person2.sayName); // true
3.1理解原型对象
只要创建一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性。这个属性包含一个指向prototype属性所在函数的指针。Person.prototype.constructor指向Person。
当调用构造函数创建一个实例,实例的内部将包含指向构造函数的原型对象的指针(内部属性),称为[[Prototype]]。在Firefox、Safari和Chrome通过_proto_访问。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
下图展示了各个对象之间的关系:
Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型中除了constructor属性,还有其他添加的属性。Person实例中都包含一个内部属性,该属性仅仅指向了Person.prototype,它们和构造函数没有直接关系。
虽然无法访问[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。
alert(Person.prototype.isPrototypeOf(person1)); // true alert(Person.prototype.isPrototypeOf(person2)); // true
在读取某个对象的属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。搜索首先从对象实例本身出发开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找给定名字的属性。如果在原型对象中找到了这个属性,则返回属性的值。
可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果在实例中添加一个与实例原型中的一个属性同名的属性,该属性将会屏蔽原型中的属性。
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = "20"; Person.prototype.job = "programmer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = "oooo"; alert(person1.name); // "oooo" alert(person2.name); // "xxyh"
上例中,person1中的name属性屏蔽了原型中的name属性。
当对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。这也就是说,这个属性的存在会阻止对原型中那个属性的访问。使用delete可以完成删除实例属性。
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = "20"; Person.prototype.job = "programmer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = "oooo"; alert(person1.name); // "oooo" alert(person2.name); // "xxyh" delete person1.name; alert(person1.name); // "xxyh"
hasOwnProperty()可以检测一个属性是存在于实例中,还是存在于原型中。
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = "20"; Person.prototype.job = "programmer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name")); // false person1.name = "oooo"; alert(person1.hasOwnProperty("name")); // true
下图展示了不同情况的实现与原型的关系:
3.2原型与in操作符
使用in操作符的方式:单独使用、在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = "20"; Person.prototype.job = "programmer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); alert("name" in person1); // true person1.name = "oooo"; alert("name" in person1); // true
结合前面的hasOwnProperty()特点,可以确定某个属性是原型中的属性还是实例中的属性。如果in操作符返回true而hasOwnProperty返回false,则属性是原型中的属性。
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name)&& (name in object); }
接下来,看看hasPrototypeProperty()的用法:
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = "20"; Person.prototype.job = "programmer"; Person.prototype.sayName = function () { alert(this.name); }; var person = new Person(); alert(hasPrototypeProperty(person, "name")); // true person.name = "oooo"; alert(hasPrototypeProperty(person, "name")); // false
在使用for-in循环时返回的是所有能够通过对象访问的、可枚举的属性,包括实例中的属性和原型中的属性。屏蔽了原型中不可枚举数据(即[[Enumerable]]标记为false的属性)的实例属性也会在for-in中返回,因为根据规定,开发人员定义的属性都是可枚举的。
要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法。
function Person() { } Person.prototype.name = "xxyh"; Person.prototype.age = "20"; Person.prototype.job = "programmer"; 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 = "oooo"; p1.age = 15; var p1_keys = Object.keys(p1); alert(p1_keys); // name, age
如果需要得到所有实例属性,可以使用Object.getOwnPropertyNames()方法
var keys = Object.getOwnPropertyNames(Person.prototype); alert(keys); // "constructor,name,age,job,sayName"
3.3更简单的原型语法
为了精简输入,用一个包含所有属性和方法的对象字面量来重写整合原型对象。
function Person() { } Person.prototype = { name : "xxyh", age : 18, job : "programmer", sayName : function () { alert(this.name); } };
上面将Person.prototype设置为等于一个以对象字面量形式创建的新对象。结果相同,但是constructor属性不在指向Person了。
通过instanceof能返回正确结果,但是constructor无法确定对象的类型:
var boy = new Person(); alert(boy instanceof Object); // true alert(boy instanceof Person); // true alert(boy.constructor == Person); // false alert(boy.constructor == Object); // true
可以通过下面的方式设置constructor的值:
function Person() { } Person.prototype = { constructor : Person, name : "xxyh", age : 18, job : "programmer", sayName : function () { alert(this.name); } };
3.4原型链的动态性
由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都会反映到实例上。但是如果重写整个原型对象,结果就不同了。调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型的联系。实例中的指针仅指向原型,而不指向构造函数。
function Person() { } var boy = new Person(); Person.prototype = { constructor : Person, name : "xxyh", age : 29, job : "programmer", sayName : function () { alert(this.name); } }; boy.sayName(); // 错误
具体过程如下:
从上面可以看出,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的是最初的原型。
3.5原生对象的原型
所有原生引用类型都是在构造函数的原型上定义了方法。通过原生对象的原型,不仅可以取得默认方法,而且可以定义新方法。
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0; }; var msg = "good morning"; alert(msg.startsWith("good")); // true
3.6原型对象的问题
原型模式存在两个问题:
•在默认情况下都取得相同的属性值。
•原型中的所有属性是实例共享的
下面看一个例子:
function Person() { } Person.prototype = { constructor: Person, name: "xxyh", age : 18, job : "programmer", friends:["张三", "李四"], sayName: function () { alert(this.name); } }; var p1 = new Person(); var p2 = new Person(); p1.friends.push("王五"); alert(p1.friends); // 张三,李四,王五 alert(p2.friends); // 张三,李四,王五 alert(p1.friends == p2.friends); // true
上面通过p1.friends添加了一项,由于friends数组存在于Person.prototype中,所以在p2.friends也反映出来了。可是,实例一般都是要有属于自己的全部属性的。
组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。这样,每个实例都会有自己的一份实例属性的副本,但是同时又共享着对方法的引用。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["张三", "李四"]; } Person.prototype = { constructor: Person, sayName: function () { alert(this.name); } } var p1 = new Person("萧萧弈寒", 18, "programmer"); var p2 = new Person("魁拔", 10, "捉妖"); p1.friends.push("王五"); alert(p1.friends); // 张三,李四,王五 alert(p2.friends); // 张三,李四 alert(p1.friends == p2.friends); // false alert(p1.sayName == p2.sayName); // true
上例中,实例属性都是在构造函数中定义的,共享属性constructor和方法sayName()则是在原型中定义的。p1.friends的修改并不会影响到p2.friends的结果。
动态原型模式
动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点。这就是说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job) { // 属性 this.name = name; this.age = age; this.job = job; // 方法 if (typeof this.sayName != "function") { Person.prototype.sayName = function () { alert(this.name); } } }
这里只在sayName()方法不存在时,才会将它添加到原型中,只会在初次调用构造函数时执行。
寄生构造函数模式
这种模式的思想是创建一个函数,该函数的作用是封装创建对象的代码,然后再返回新创建的对象。
function Person(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.sayName = function () { alert(this.name); } return obj; } var boy = new Person("xxyh", 19, "programmer"); boy.sayName();
需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;构造函数返回的对象与在构造函数外部创建的对象没有不同。不能依赖instanceof操作符来确定对象类型。
稳妥构造函数模式
稳妥对象指的是没有公共属性,而且其方法也不引用this的对象。稳妥构造函数遵循与寄生构造函数类似的模式,但是有两点不同:
•新创建对象的实例方法不引用this;
•不使用new操作符调用构造函数
重写Person构造函数如下:
function Person(name, age, job) { var obj = new Object(); obj.sayName = function () { alert(name); }; return obj; }
function Person(name, age, job) { var obj = new Object(); obj.sayName = function () { alert(name); }; return obj; }