第八章 对象、类与面向对象编程 第二节——创建对象

8.2 创建对象

        虽然使用Object构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。

8.2.1 概述

        综观ECMAScript规范的历次发布,每个版本的特性似乎都出人意料。ECMAScript 5.1并没有正式支持面向对象的结构,比如类或继承。但是,正如接下来几节会介绍的,巧妙地运用原型式继承可以成功地模拟同样的行为。

        ECMAScript 6开始正式支持类和继承。ES6的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6的类都仅仅是封装了ES5.1构造函数加原型继承的语法糖而已。

注意         不要误会:采用面向对象编程模式的JavaScript代码还是应该使用ECMAScript 6的类。但不管怎么说,理解ES6类出现之前的惯例总是有益无害的。特别是ES6的类定义本身就相当于对原有结构的封装。因此,在介绍ES6的类之前,本书会循序渐进地介绍被类取代的那些底层概念。

8.2.2 工厂模式

        工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。下面的例子展示了一种按照特定接口创建对象的方式:

function createPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");

        这里,函数createPerson()接收3个参数,根据这几个参数构建了一个包含Person信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

8.2.3 构造函数模式

        前面几章提到过,ECMAScript中的构造函数是用于创建特定类型对象的。像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

        比如,前面的例子使用构造函数模式可以这样写:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    };
}

let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");

person1.sayName(); // Nicholas
person2.sayName(); // Greg

        在这个例子中,Person()构造函数代替了createPerson()工厂函数。实际上,Person()内部的代码跟createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。
  • 属性和方法直接赋值给了this。
  • 没有return。

        另外,要注意函数名Person的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在ECMAScript中区分构造函数和普通函数。毕竟ECMAScript的构造函数就是能创建对象的函数。

        要创建Person的实例,应使用new操作符。以这种方式调用构造函数会执行如下操作。

(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
(3) 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

        上一个例子的最后,person1和person2分别保存着Person的不同实例。这两个对象都有一个constructor属性指向Person,如下所示:

console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true

        constructor本来是用于标识对象类型的。不过,一般认为instanceof操作符是确定对象类型更可靠的方式。前面例子中的每个对象都是Object的实例,同时也是Person的实例,如下面调用instanceof操作符的结果所示:

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

        定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在这个例子中,person1和person2之所以也被认为是Object的实例,是因为所有自定义对象都继承自Object(后面再详细讨论这一点)。

let Person = function(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    };
}

let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");

person1.sayName(); // Nicholas
person2.sayName(); // Greg

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

        在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有new操作符,就可以调用相应的构造函数:

function Person() {
    this.name = "Jake";
    this.sayName = function() {
        console.log(this.name);
    };
}

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

person1.sayName(); // Jake
person2.sayName(); // Jake

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

1、构造函数也是函数

        构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数。比如,前面的例子中定义的Person()可以像下面这样调用:

// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"

// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到window对象
window.sayName(); // "Greg"

// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"

        这个例子一开始展示了典型的构造函数调用方式,即使用new操作符创建一个新对象。然后是普通函数的调用方式,这时候没有使用new操作符调用Person(),结果会将属性和方法添加到window对象。这里要记住,在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this始终指向Global对象(在浏览器中就是window对象)。因此在上面的调用之后,window对象上就有了一个sayName()方法,调用它会返回"Greg"。最后展示的调用方式是通过call()(或apply())调用函数,同时将特定对象指定为作用域。这里的调用将对象o指定为Person()内部的this值,因此执行完函数代码后,所有属性和sayName()方法都会添加到对象o上面。

2、构造函数的问题

        构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言,person1和person2都有名为sayName()的方法,

你可能感兴趣的:(javascript,开发语言,ecmascript)