[关闭]
@15152278073 2018-03-30T16:48:09.000000Z 字数 5727 阅读 527

创建对象(上)

JS


Object构造函数或对象字面量都可以用来创建单个对象,但会产生大量重复代码.为了解决这个问题,人们开始使用工厂模式的一种变体.

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式。这种模式创建具体对象的过程。考虑到在JS中无法创建类,开发人员就发明了一种函数。用函数来封装特定接口创建对象的细节,如下:

  1. // 工厂模式
  2. function createPerson(name, age, job) {
  3. var o = new Object();
  4. o.name = name;
  5. o.age = age;
  6. o.job = job;
  7. o.sayName = function () {
  8. console.log(this.name)
  9. }
  10. return o;
  11. }
  12. var p1 = createPerson("Nick", 29 , "Sofe Engine")
  13. var p2 = createPerson("John",24,"Director")
  14. p1.sayName() //Nick
  15. p2.sayName() //John

工厂模式虽然解决了创建多个对象的问题,但没有解决对象识别的问题(怎样知道一个对象的类型)。

构造函数模式

JS中可以用构造函数来创建对象。同时,也可以自定义构造函数来创建对象的属性和方法。

  1. function Person(name, age, job){
  2. this.name = name;
  3. this.o.age = age;
  4. this.o.job = job;
  5. this.sayName = function(){
  6. console.log(this.name)
  7. }
  8. }
  9. var p1 = Person("Nick", 29 , "Sofe Engine")
  10. var p2 = Person("John",24,"Director")

这段代码与createPerson()有以下不同:

另外:函数名Person()首字母大写,非构造函数首字母小写(OOP语法)。
要使用Person实例,必须使用new操作符。这种方式调用构造函数会经历以下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋值给新对象(this就指向了这个新对象);
  3. 执行构造函数中的代码。
  4. 返回新对象。

这两个对象都有一个constructor属性,指向Person,如下:

  1. console.log(p1.constructor == Person) //true
  2. console.log(p2.constructor == Person) //true

对象的属性最初是用来标识对象类型的。通过构造函数,p1p2即是Object类型,也是Person类型。

构造函数定义在全局的对象(浏览器中为windows对象)中。

1.将全局对象当作函数

构造函数与其他函数唯一的区别就在于调用方式不同。当然,构造函数也是函数,不存在定义的特殊语法。任何函数,通过new来调用,就作为构造函数;所以Person可以以下方式调用:

  1. //a.构造函数
  2. var p3 = new Person("nickoo",28,"PHPer");
  3. p3.sayName(); //nickoo
  4. //b.普通函数
  5. Person("JJhon",20,"Javaer"); //添加到windows对象
  6. window.sayName(); //JJhon
  7. //c.在另一个对象的作用域中调用
  8. var o = new Object();
  9. Person.call(o,"Kate",25,"Nurse")
  10. o.sayName();
a. 例子展示了构造函数的典型用法。
b. 例子展示了不用new操作符会出现什么结果:属性和方法都添加给了windows对象。所以在调用完后,可以通过windows对象调用。
c. 例子通过使用call()/apply()方法在某个特殊对象的作用域中(对象o)调用Person()函数。因此,o就有了所有属性和sayName()方法。

2. 构造函数的问题

构造函数的问题,就在于每个方法都要在每个实例上重新创建一遍。想想:函数是对象,因此创建对象时,函数也被创建一次。以下代码可以证明:

  1. console.log(p1.sayName == p2.sayName) //false

原型模式

每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型的好处是可以让所有对象的实例共享它的属性何方法。

  1. function PersonPro() {
  2. }
  3. PersonPro.prototype.name = "Nick_Pro";
  4. PersonPro.prototype.age = 29;
  5. PersonPro.prototype.job = "Java_Pro";
  6. PersonPro.prototype.sayName = function () {
  7. console.log(this.name)
  8. }
  9. var per1 = new PersonPro()
  10. per1.sayName() //Nick_Pro
  11. var per2 = new PersonPro()
  12. per1.sayName() //Nick_Pro
1. 理解原型对象

无论什么时候,只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。所有原型对象都会获得一个constructor属性,属性包含一个指向prototype属性的函数指针。
创建自定义函数后,其原型默认只会取得constructor属性;至于其他对象,都是从Object继承而。,虽然在所有实现都无法访问[[prototype]],但可以通过isPrototypeOf()来确定对象之间是否存在关系。

  1. console.log(PersonPro.prototype.isPrototypeOf(per1))
  2. console.log(PersonPro.prototype.isPrototypeOf(per2))
  3. //ECMA 新增的Object.getPrototype()
  4. console.log(Object.getPrototypeOf(per1))
  5. console.log(Object.getPrototypeOf(per2).name)

使用Object.getPrototypeOf()可以方便的取得一个对象的原型,这在利用原型实现继承的情况下是非常重要的。虽然可以通过对象实例访问保存在原型的值,但却不能通过对象实例重写原型中的值。如:

  1. var per3 = new PersonPro();
  2. per3.name = "hello"
  3. console.log(per1.name) // Nick_Pro 原型
  4. console.log(per3.name) //hello 实例
  5. per3.name = null;
  6. console.log(per3.name) // 实例
  7. delete per3.name //delete 断开联系
  8. console.log(per3.name) // Nick_Pro 原型

通过hasOwnProperty(),可以知道属性是否是实例属性or原型属性。

Object.getOwnPropertyDescriptor()只能用于实例属性。要取得原型描述符,必须在原型对象上调用Object.getOwnPropertyDescriptor()方法。

2. 原型与in操作符

in操作符有两种方式。单独使用和在for-in中使用。单独使用,in操作符会在通过对象能够访问给定属性时返回true,无论属性存在于实例中还是原型中。

  1. var p1 = new PersonIN()
  2. var p2 = new PersonIN()
  3. console.log(p1.hasOwnProperty("name")) //false
  4. console.log("name" in p1) //true
  5. console.log("name" in p2) //true
  6. p1.name = "jhon"
  7. console.log("name" in p1) //true

使用for-in循环时,返回的是能够通过对象访问的、可枚举的(enumrated)属性,包括存在属性和原型中。屏蔽了原型中不可枚举的属性也会在枚举中出现。

  1. Object.defineProperty(p1, "age", {
  2. value: 18,
  3. enumerable: false
  4. })
  5. console.log("age" in p1) //true
  6. for (let p in p1) {
  7. console.log(`prop : ${p}`) //name , sayName
  8. }
  9. console.log(Object.keys(p1)) //["name"]
  10. console.log(Object.getOwnPropertyNames(p1)) //["name", "age"]
  11. console.log("================")
  12. console.log(Object.keys(p1.__proto__)) //["name", "sayName"]
  13. console.log(Object.getOwnPropertyNames(p1.__proto__)) //["constructor", "name", "sayName"]
3. 更简单的原型语法

更常用的做法是用一个对象字面量来重写整个原型对象。

  1. var p2 = new PersonOut()
  2. PersonOut.prototype = {
  3. name:"Nick",
  4. age:19,
  5. job:"Java",
  6. sayName:function(){
  7. console.log(this.name)
  8. }
  9. }

虽然最终结果相同,但constructor属性不再指向Person了。每创建一个对象,就创建它的prototype
对象,这个对象获得constructor属性。而我们完全重写了默认的prototype对象,因此,constructor属性也指向了新对象的constructor属性。

  1. var po1 = new PersonOut()
  2. console.log(po1 instanceof PersonOut) // true
  3. console.log(po1.constructor == PersonOut) //falese
4.原型的动态性

由于原型在找值过程中是一次搜索,因此我们对原型对象的修改立即能够从实例上反映出来,即使新创建对象后修改原型也是如此。

  1. var friend = new Person()
  2. Person.prototype.sayHi= function(){
  3. console.log("hi")
  4. }
  5. friend.sayHi() //hi

但是如果重写原型对象就不一样了。我们知道,创建构造函数时,会创建一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型的联系。

实例中的指针仅指向原型,而不指向构造函数。

  1. function PersonC(){};
  2. var pc = new PersonC()
  3. PersonC.prototype = {
  4. constructor:PersonC,
  5. name : 'nick',
  6. sayName :function(){
  7. console.log(this.name)
  8. }
  9. };
  10. pc.sayName() // Uncaught TypeError: pc.sayName is not a function
5. 原生对象的原型

所有原生的引用类型都采用原型模式创建的。如:ArrayString等,并在基础上定义了方法。如Arraysort()StringsubString()方法。使用者也可以自定义新的方法。

尽管可以这么做,但不推荐。在原生对象中添加方法,这样会导致可能的命名冲突,也可能意外导致重写原生方法。参阅:JavaScript 社区由一个库引发的“smoosh门”事件到底怎么回事?

6. 原型对象的问题

原型模式省略了构造函数出始化环节,所以所有实例在默认情况下都是相同属性。原型属性最大的问题是由其共享本质所导致的。

原型中的属性被所有实例共享,对于函数非常合适。对于包含引用类型的属性来说,就非常不合适了。

  1. function PersonArray(){};
  2. PersonArray.prototype.name = "nick"
  3. PersonArray.prototype.friends = ["bob","dlean"]
  4. var pa1 = new PersonArray();
  5. var pa2 = new PersonArray();
  6. pa1.friends.push("Vani");
  7. console.log(pa1.friends); //["bob", "dlean", "Vani"]
  8. console.log(pa2.friends); //["bob", "dlean", "Vani"]
  9. console.log(pa1.friends === pa2.friends); //true

组合使用构造函数模式和原型模式

创建自定义类型的常见方式,就是组合使用函数模式和原型模式.构造函数使用自定义实例属性,原型模式用于定义方法和共享的属性。这种模式支持向构造函数传递参数。

  1. function PersonFinal(name, age) {
  2. this.name = name
  3. this.age = age
  4. this.friends = ["shely", "yang"]
  5. }
  6. PersonFinal.prototype = {
  7. constructor: PersonFinal,
  8. sayName: function () {
  9. console.log(this.name)
  10. }
  11. }
  12. var pf1 = new PersonFinal("zane", 22)
  13. var pf2 = new PersonFinal("Dom", 92)
  14. pf1.friends.push("cindy")
  15. console.log(pf1.friends) //["shely", "yang", "cindy"]
  16. console.log(pf2.friends) //["shely", "yang"]
  17. console.log(pf1.sayName == pf2.sayName) //true

这种构造函数与原型混成的模式是JS中最广泛与认同度最高的一种。可以说用来自定义的一种默认模式。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注