创建对象
虽然Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体
工厂模式
这种模式抽象了创建具体对象的过程。考虑到在ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节
示例:
// 工厂模式 function createPerson(name, age, job) { var persion = new Object(); persion.name = name; persion.age = age; persion.job = job; persion.sayName = function() { console.log(this.name); } return persion; } var persion1 = createPerson("zz", 20, "大一"); var persion2 = createPerson("pp", 22, "大三");
缺点 :
- 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式
ECMAScript 中的构造函数可用来创建特定类型的对象.
可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
示例:
// 构造函数模式 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); } } var persion3 = new Person("cc", 20, "大一"); var persion4 = new Person("dd", 22, "大二"); // 这两个对象都有一个constructor(构造函数)属性,该属性指向Person, console.log(persion3.constructor == Person);//true
工厂模式与构造函数模式不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了this 对象;
- 没有return 语句。
此外,还应该注意到函数名Person 使用的是大写字母P。
new操作符调用构造函数经历的4个步骤:
- (1) 创建一个新对象;
- (2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象);
- (3) 执行构造函数中的代码(为这个新对象添加属性);
- (4) 返回新对象。
将构造函数当作函数使用
- 构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
- 任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;
- 而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样
// 当作构造函数使用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); //"Nicholas" // 作为普通函数调用 Person("Greg", 27, "Doctor"); // 添加到window window.sayName(); //"Greg" // 在另一个对象的作用域中调用 var o = new Object(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); //"Kristen"
构造函数的问题:
问题:
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1 和person2 都有一个名为sayName()的方法,但那两个方法不是同一个Function 的实例。
alert(person3.sayName == person4.sayName); //false
解决:
通过把函数定义转移到构造函数外部来解决这个问题。
例如:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName(){ alert(this.name); }
person1 和person2 对象就共享了在全局作用域中定义的同一个sayName()函数。
仍然存在问题:
在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。
原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
理解原型对象
- 只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
- 在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype. constructor 指向Person。
- [[Prototype]], 即__proto__ 这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
上图展示了Person 构造函数、Person 的原型属性以及Person 现有的两个实例之间的关系。在此,Person.prototype 指向了原型对象,而Person.prototype.constructor 又指回了Person。原型对象中除了包含constructor 属性之外,还包括后来添加的其他属性.
实例添加属性
- 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。
- 即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。
- 不过,使用delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,
检测属性是在原型还是实例中
- 使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object 继承来的)只在给定属性存在于对象实例中时,才会返回true
- hasOwnProperty()只在属性存在于实例中时才返回true,
-
有两种方式使用in 操作符:单独使用和在for-in 循环中使用。在单独使用时,in 操作符会在通过对象能够访问给定属性时返回true,
- in 操作符只要通过对象能够访问到属性就返回true
Object.keys()方法。
- 这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
- 无论它是否可枚举,都可以使用Object.getOwnPropertyNames()
// 原型模式 function Person2() { } Person2.prototype.name = "Nicholas"; Person2.prototype.age = 29; Person2.prototype.job = "Software Engineer"; Person2.prototype.sayName = function () { console.log(this.name); }; var person5 = new Person2(); person5.sayName(); //"Nicholas" ——来自原型 var person6 = new Person2(); person6.name = "ff"; person6.sayName(); //ff ——来自实例 // hasOwnProperty可以判断属性是否是存在于实例中 console.log(person6.hasOwnProperty("name"));//true console.log(person6.hasOwnProperty("age"));//false // 新对象的这些属性和方法是由所有实例共享的 console.log(person5.sayName == person6.sayName); //true console.log(Person2.prototype);//{name: "Nicholas", age: 29, job: "Software Engineer", sayName: ƒ, constructor: ƒ} // 内部有一个指向Person2.prototype 的指针,因此都返回了true console.log(Person2.prototype.isPrototypeOf(person5));//true person6.name = null; person6.sayName();//null // 使用delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性 delete person6.name; person6.sayName();//Nicholas // Object.keys() 接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。 console.log(Object.keys(Person2.prototype));//["name", "age", "job", "sayName"] // Object.getOwnPropertyNames() 不限于枚举类型 console.log(Object.getOwnPropertyNames(Person2.prototype));// ["constructor", "name", "age", "job", "sayName"] // 更简单的原型链语法 function Person3() { } Person3.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", friends: ["aa","bb","cc"], sayName: function () { alert(this.name); } }; var person7 = new Person3(); var person8 = new Person3(); person7.name = "person7"; console.log(person7.name);//person7 console.log(person8.name);//Nicholas // 引用属性 出问题 person7.friends.push("ee"); console.log(person7.friends);//["aa", "bb", "cc", "ee"] console.log(person8.friends);//["aa", "bb", "cc", "ee"]
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。
- 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
- 结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。
- 另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
// 组合使用构造函数模式和原型模式 // 构造函数模型 function Person4(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } // 原型链模型 Person4.prototype = { constructor: Person4, sayName: function () { console.log(this.name); } } var person11 = new Person4("Nicholas", 29, "Software Engineer"); var person22 = new Person4("Greg", 27, "Doctor"); person11.friends.push("Van"); console.log(person11.friends); //"Shelby,Count,Van" console.log(person22.friends); //"Shelby,Count" person11.sayName = function() { console.log("person11.sayName"); } // person11.sayName();//person11.sayName // person22.sayName();//Greg console.log(person11.friends === person22.friends); //false // console.log(person11.sayName === person22.sayName); //false console.log(person11.sayName === person22.sayName); //true