JavaScript OOP:创建对象

0、基本方法

JavaScript创建自定义对象的基本方法就是new一个Object的实例,然后为其添加相应的属性和方法。例如创建一个person对象:

var person = new Object();

person.name = "Morty";
person.age = 14;
person.sayName = function() {
  console.log("this.name");
}
另一种常见的定义方法为字面量语法。同样的创建一个person对象,字面量语法可以这样写:

var person = {
  name: "Morty",
  age: 14,
  sayName: function() {
    console.log(this.name);
  }
};
两种方法本质相同。

1、工厂模式

当需要大量对象时,基本的创建方法会产生大量重复代码。工厂模式抽象了创建对象的过程,在JavaScript中表现为利用函数来封装创建对象的细节。例如定义一个createPerson函数:
function createPerson(name, age) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayName = function() {
      console.log(this.name);
    };
    return obj;
  }
   
  var person1 = createPerson("Morty", 14);
  var person2 = createPerson("Rick", 70);
函数createPerson()能够根据参数来构建person对象,并将其返回给调用者。

2、构造函数

JavaScript可以使用自定义构造函数来创建对象。例如将person的构造函数定义如下:
function Person(name, age) { //构造函数一般首字母大写,非构造函数首字母小写
  this.name = name;
  this.age = age;
  this.sayName = function() {
    console.log(this.name);
  };
}

var person1 = new Person("Morty", 14);
var person2 = new Person("Rick", 70);
对比构造函数Person()和工厂模式的createPerson()函数,可以发现二者整体框架大致相同,构造函数的特殊之处在于:
  • 没有显式创建对象,而是直接将传入的属性和方法赋给了this对象
  • 没有return语句
  • 调用构造函数必须使用new操作符
通过构造函数创建的person1和person2分别保存着Person的不同实例。并且这两个对象都有一个 constructor(构造函数)属性,该属性指向Person:
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true
constructor属性用来标识对象类型。确定对象类型的更广泛做法是使用 instanceof操作符:
console.log(person1 instanceof Object); //true person1和person2都是Person的实例,
console.log(person1 instanceof Person); //true 同时也都是Object的实例(JavaScript中所有对象都继承自Object)。
使用构造函数的意义在于: 可以将其实例标识为一种特定类型。这也是构造函数之于工厂方法的优势。
 
下面总结一下构造函数的特点:
2.1、构造函数与一般函数
构造函数也是函数,本质与其他函数没有不同,因此定义构造函数也不存在特殊语法。
构造函数与其他函数唯一的区别在于调用方式不同:构造函数通过 new操作符调用才可以发挥构造作用。因此:
  • 不通过new操作符调用的构造函数与一般函数没有区别
  • 任何函数只要通过new操作符调用,它就可以作为构造函数
使用构造函数Person()来说明上述两点:
// 1,Person()作为构造函数使用
var person = new Person("Morty", 14);
person.sayName(); //"Morty"

// 2,Person()作为一般函数使用(use strict模式下不允许)
Person("Rick", 70);
window.sayName(); //"Rick"

// 3,在另一个对象的作用域中调用
var obj = new Object();
Person.call(obj, "Summer", 17);
obj.sayName();  //"Summer"
1,在使用new操作符调用构造函数时,创建了一个新对象;
2,在不使用new操作符而直接调用Person()时,相应的方法和属性都添加给了window对象,因为全局作用域中this总是指向Global对象(浏览器中为window对象);
3,当然也可以使用call()(或apply())在某个对象的作用域中调用Person()函数。
2.2、构造函数的缺陷
考虑上例中的sayName()方法,person1和person2分别有各自的sayName()方法,但二者的sayName()方法并不是一个:
console.log(person1.sayName === person2.sayName); //false
问题在于每次调用构造函数Person(),都会实例化一个新对象,同时也会实例化一个新的sayName()方法(JavaScript中函数也是对象)。实际上构造函数等价于如下写法:
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = new Function("console.log(this.name)");
}
然而创建两个功能重复的sayName是没有必要的,为解决这个问题,可以将sayName()方法放在构造函数外:
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = sayName;
}

function sayName() {
  console.log(this.name);
}
如此定义的外部sayName()方法通过this机制可被person1和person2等实例共享。然而对象需要很多方法时,就要定义很多个外部函数,自定义类型的封装性被严重破坏。为了解决这些问题,我们引入原型模式。

3、原型模式

JavaScript每个函数都有一个prototype(原型)属性。prototype指向一个对象,这个对象的作用是包含可以由各类型的所有实例共享的属性和方法,即prototype指向的是实例的原型对象。例如上例可以使用原型模式改写为:
function Person() {
}

Person.prototype.name = "Morty";
Person.prototype.age = 14;
Person.prototype.sayName = function() {
  console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();

console.log(person1.sayName === person2.sayName); //true

将sayName()方法和其他各属性直接放在了Person的prototype属性中。虽然此时构造函数Person()函数体为空,但是仍然可以通过调用构造函数来创建新对象,并且各个实例共享prototype中的属性和方法。

使用原型对象的好处就在于让所有对象实例共享原型包含的属性和方法,而不必在构造函数中定义对象实例的信息。

下面总结ECMAScript原型对象的性质

3.1、原型对象工作原理

每当一个新函数被创建,JavaScript就会为该函数创建一个prototype属性。这个属性指向该函数的原型对象。

而原型对象会获得一个constructor(构造函数)属性。这个属性指向相应prototype属性所在函数。

以Person()构造函数为例,各个对象之间的关系如图:


上图展示了构造函数Person,Person的原型对象和Person两个实例person1、person2之间的关系。

Person.prototype指向了原型对象,Person.prototype.constructor又指回了Person。原型对象中自动生成的只有constructor属性,除此以外可以自行添加各种属性和方法,如上例中name、age等。

Person的每个实例中都包含一个内部属性[[prototype]],该属性仅仅指向Person的原型对象,即该属性与constructor无直接关系。//ECMA标准中[[prototype]]是不可见的,但是某些浏览器(如chrome)支持__proto__方法操作[[prototype]]。

我们无法访问到[[prototype]],但可以通过isPrototypeOf()方法确定来确定原型对象Person.prototype和实例person1、person2之间的关系:

console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Person.prototype.isPrototypeOf(person2)); //true
或者通过 getPrototypeOf()方法直接获取某实例的[[prototype]]值(ECMAScript 5):

console.log(Object.getPrototypeOf(person1) === Person.prototype); //true
console.log(Object.getPrototypeOf(person1).name); //"Morty"

虽然实例中都不包含属性和方法,但是我们可以通过person1.sayName()这种形式来调用。这是由查找对象属性的过程实现的:每当需要读取某个实例的某个属性时,如要读取person1的name,如果在该实例中有具体的属性则直接读取;如果没有,则在[[prototype]]所指的原型对象中查找。

如果在实例中又定义了某个与原型对象重名的属性,则默认将原型对象中的属性屏蔽(而非修改,原型中属性依然存在),而删除实例中的属性,原型中属性又重新暴露出来。可以通过hasOwnProperty()方法检测某个属性是存在于实例中还是存在于原型中:

function Person() {
}

Person.prototype.name = "Morty";
Person.prototype.age = 14;
Person.prototype.sayName = function() {
  console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name")); //false

person1.name = "Rick";  //覆盖原型中name属性
console.log(person1.name);  //"Rick"
console.log(person1.hasOwnProperty("name"));  //true

delete person1.name;  
console.log(person1.name);  //"Morty"
console.log(person1.hasOwnProperty("name")); //false
JavaScript的 in操作符hasOwnProperty()方法同时使用可以确定属性到底是属于实例对象中还是原型对象中。in操作符在通过对象能够访问给定属性时返回true,而不管属性属于实例还是原型。

function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}

3.2、更简洁的原型语法
上述方法每次添加一个属性或方法就要写一遍Person.prototype,为了让代码更简洁、更体现封装性,常见的写法是用一个包含所有属性和方法的对象字面量重写整个原型对象:

function Person() {
}

Person.prototype = {
  name: "Morty",
  age: 14,
  sayName: function() {
    console.log(this.name);
  }
};
两种定义原型对象的方法基本相同,唯一的例外在于:constructor属性不再指向Person了。

前文介绍过,每创建一个函数,都会自动生成其prototype对象,这个对象也会自动获得constructor属性。而现在这种语法相当于将默认prototype对象重写了,因此constructor属性也变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。但是instanceof操作符能返回正确结果:

console.log(person3 instanceof Object); //true
console.log(person3 instanceof Person); //true

console.log(person3.constructor === Person); //false
console.log(person3.constructor === Object); //true

可以在对象字面量中添加 constructor: Person, 将constructor指向Person(会修改constructor的可枚举性)。

3.3、原型的动态性
前文说到在原型中查找属性是一次搜索过程,因此我们对原型对象的任何修改都能在实例中动态体现出来:

function Person() {
}

var person4 = new Person();

Person.prototype.run = function() {
  console.log("runing...");
}

person4.run();  //running...
上例中先创建了实例person4,后修改了原型,修改后的原型依然可以在实例中反映出来。

注意,虽然可以随时修改原型属性和方法,并且能在原型中动态反映,但若使用3.2小节中的原型语法——重写整个原型对象,这种动态性就会失效了:

function Person() {
}

var person4 = new Person();

Person.prototype = {
  name: "Morty",
  age: 14,
  sayName: function() {
    console.log(this.name);
  }
};

person4.sayName();  //error
上例中先创建实例后重写原型,然而重写的原型并不能在实例中反映出来。因为每个实例被创建时自动生成的 [[prototype]]属性指向最初自动生成的原型,而非重写后的原型。

下图反映了重写原型前后[[prototype]]指向的变化:


3.4、原生对象的原型

除了自定义类型,JavaScript所有的原型引用类型(如Object、Array、String...)都采用原型模式创建。所有原生引用类型都在其构造函数的原型上定义了相应方法。如Array.prototype中定义了sort()方法,在String.prototype中定义了substring()方法等。

除了使用默认方法,也可以对原生类型自定义新方法。例如可以给原生String类型添加一个startWith()方法:

String.prototype.startsWith = function(str) {
  return this.indexOf(str) === 0;
};

var text = "GNU IS NOT UNIX";
console.log(text.startsWith("GNU")); //true
3.5原型模式的缺陷
原型模式的缺陷实际上也是其最大的特点:共享。原型中的各种属性、方法都是被实例共享的,这对于多数情况是非常合适的。尤其是实例对原型中方法的共享是原型模式最常见的应用场景,对原型中属性的共享也可以被接受(毕竟能在实例中添加同名属性来屏蔽原型中的属性)。然而在对于包含 引用类型的属性进行共享时,问题就会出现:
function Person() {
}

Person.prototype = {
  constructor: Person,
  name: "Morty",
  age: 14,
  classmates: ["Jessica", "Brad"],
  sayName: function() {
    console.log(this.name);
  }
}

var person1 = new Person();
var person2 = new Person();

person1.classmates.push("Ethan");

console.log(person1.classmates);  //"Jessica", "Brad", "Ethan"
console.log(person2.classmates);  //"Jessica", "Brad", "Ethan"

console.log(person1.classmates === person2.classmates); //true
实例person1和person2共享了classmates属性,这并不是我们所期望的结果。原型模式不能体现属于某个特定实例的自有属性,因此原型模式一般不单独使用。

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

创建自定义对象更为广泛的方式是将构造函数和原型模式组合使用。

  • 构造函数用于定义实例属性
  • 原型模式用于定义方法和共享的属性
这样做使得每个实例都有属于自己的属性,又同时共享对方法的引用,最大程度节省了内存。

组合两种方法重写上例:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.classmates = ["Jessica", "Brad"];
}

Person.prototype = {
  constructor: Person,
  sayName = function() {
    console.log(this.name);
  }
};

var person1 = new Person("Morty", 14);
var person2 = new Person("Rick", 70);

person1.classmates.push("Ethan");

console.log(person1.classmates);  //"Jessica", "Brad", "Ethan"
console.log(person2.classmates);  //"Jessica", "Brad"

console.log(person1.classmates === person2.classmates); //false
console.log(person1.sayName === person2.sayName); //true

这种组合使用构造函数和原型模式的定义方法是JavaScript中最广泛的创建自定义类型的方法。


参考资料《JavaScript高级程序设计(第3版)》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值