构造函数模式(Constructor Pattern),就是通过自定义的构造函数来创建对象。

JS 中创建值有两种方式

  • 字面量表达式

  • 构造函数模式

  1. var obj = {} //=> 字面量
  2. var obj = new Object(); //=> 构造函数模式
  3. //=> 不管是哪一种方式创造出来的都是 Object 类的实例,而实例之间是独立分开的
  4. //=> 字面量模式就是 JS 中的单例模式

基本类型通过两种方式创建出来的值是不一样的

  • 基于字面量方式创建出来的值是基本类型值

  • 基于构造函数创建出来的值是引用类型

  1. var num1 = 12;
  2. var num2 = new Number(12);
  3. typeof num1 //=> 'number'
  4. typeof num2 //=> 'object'

1. 构造函数

基于构造函数创建自定义类

  • 在普通函数的基础上,通过 new 执行,就不是普通函数执行了,而是构造函数执行,当前的函数名称为 类名,接收的返回值结果是当前类的一个实例

  • 按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

  • 这种构造函数模式,主要用于组件、类库、插件、框架等的封装,平时编写业务逻辑一般不这样处理

  1. function Person(name, age, job) {
  2. this.name = name;
  3. this.age = age;
  4. this.job = job;
  5. this.sayName = function() {
  6. alert(this.name);
  7. };
  8. }
  9. //=> 两个实例是独立分开的,互不影响
  10. var person1 = new Person("Nicholas", 29, "Software Engineer");
  11. var person2 = new Person("Greg", 27, "Doctor");

构造函数模式相对于工厂模式:

  • 没有显式的创建对象

  • 直接将属性和方法赋给了 this 对象

  • 没有 return 语句

  • 它的实例通过 constructor 属性被标识为一种特定的类型

构造函数的运行机制
要创建 Person 对象,必须使用 new 操作符,以这种方式调用构造函数实际上会经历 4 个步骤:

  • 创建一个新对象

  • 将构造函数的作用域赋给新对象(因此 this 就指向了新对象)

  • 执行构造函数中的代码(为这个对象添加私有属性和方法)

  • 返回新对象

2. constructor & instanceof

创建的 Person 实例对象,都有一个 constructor 属性,指向 Person。(实际这个属性是在原型上的)

这个属性最初是用来标识对象类型的,但是,在检测对象类型时,还是使用 instanceof 操作符更可靠一些。

instanceof 操作符:检测某一个实例是否隶属于这个类。

  1. alert(person1.constructor == Person); //true
  2. alert(person2.constructor == Person); //true
  3. //=> 所有对象都会是 Object 的实例
  4. alert(person1 instanceof Object); //true
  5. alert(person2 instanceof Person); //true
  6. alert(person1 instanceof Object); //true
  7. alert(person2 instanceof Person); //true

创建自定义的构造函数意味着将它的实例标识为一种特定的类型,这正是构造函数模式胜过工厂模式的地方。这两个实例对象之所以同时也是 Object 的实例,是因为所有对象均继承自 Object

3. 构造函数模式的问题

3.1 将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用的方式不同。不过,构造函数也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就是构造函数,而任何函数,如果不通过 new 来调用,就跟普通函数没什么区别。

  1. // 当作构造函数使用
  2. var person = new Person("Nicholas", 29, "Software Engineer");
  3. person.sayName(); //"Nicholas"
  4. // 当作普通函数调用
  5. Person("Greg", 27, "Doctor"); //添加到 window
  6. window.sayName(); //"Greg"
  7. // 在另一个对象的作用域中调用
  8. var o = new Object();
  9. Person.call(o, "Kristen", 25, "Nurse");
  10. o.sayName(); //"Kristen"

不使用 new 操作符调用 Person() 会导致属性和方法都被添加到 window 对象中,因为当在全局作用域中调用一个函数时,this 对象总是指向 Global 对象(在浏览器就是 window 对象)

也可以使用 call() 或者 apply() 在某个特殊对象的作用域中调用 Person() 函数。

3.2 构造函数的问题

使用构造函数的主要问题就是每一个方法都要在每一个实例上重新创建一遍

在前面的例子中,person1person2 都有一个名为 sayName() 的方法,但是这两个方法不是同一个 Function 的实例。因为在 ECMAScript 中函数也是对象,每定义一个函数,也就实例化了一个对象。
从逻辑角度讲,构造函数还可以这样定义:

  1. function Person(name, age, job) {
  2. this.name = name;
  3. this.age = age;
  4. this.job = job;
  5. this.sayName = new Function("alert(this.name)");
  6. }

以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建 Function 新实例的机制仍然是相同的。因此,不同实例的同名函数是不相等的。
创建两个完成同样任务的 Function 实例是没有必要的,可以通过把函数定义转移到构造函数外部来解决这个问题。

  1. function Person(name, age, job) {
  2. this.name = name;
  3. this.age = age;
  4. this.job = job;
  5. this.sayName = sayName;
  6. }
  7. function sayName() {
  8. alert(this.name);
  9. }

这样做确实解决了两个函数做同一件事情的问题,但是新的问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人难以接受的是,如果对象需要定义很多方法,那么就要定义很多全局函数,这个自定义的引用类型就毫无封装性可言。