JavaScript运算符new

161 阅读6分钟

javascript-15.png

面向对象编程是目前主流的编程范式,它将世界抽象为对象,各对象之间分工合作。

编程范型、编程范式或程序设计法(英语:Programming paradigm),是指软件工程中的一类典型的编程风格。常见的编程范型有:函数式编程、指令式编程、过程式编程、面向对象编程等等。编程范型提供并决定了程序员对程序执行的看法。

在面向对象编程中,程序员认为程序是一系列相互作用的对象,由于方法论的不同,面向对象编程又分为基于类编程和基于原型编程,而在函数式编程中一个程序会被看作是一个无状态的函数计算的序列。

每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发。

  • 对象是单个实物的抽象:当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
  • 对象是一个容器,封装了属性和方法:属性是对象的状态,方法是对象的行为。

构造函数

面向对象编程的第一步,就是要生成对象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。面向对象编程语言如 C++,都有“类”这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。构造函数就是一个普通的函数,但具有自己的特征和用法。

  • 函数体内部使用了 this 关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必须使用 new 命令。
function F () {
  this.name = 'F'
}

const f = new F()
console.log(f)  // F { name: 'F' }

const A = () => {}
const a = new A()
// TypeError: A is not a constructor

new命令

创建一个用户自定义的对象需要两步:

  • 通过编写函数来定义对象类型。
  • 通过 new 来创建对象实例。

new 命令的作用,就是执行构造函数,返回一个实例对象。

上面代码通过 new 命令,让构造函数 F 生成一个实例对象,保存在变量 f 中。实例对象,从构造函数 F 得到了 name 属性。new 命令执行时,构造函数内部的 this,就代表了新生成的实例对象,this.name 表示实例对象有一个 name 属性,值是 F

使用 new 命令时,根据需要,构造函数也可以接受参数。

function F (name) {
  this.name = name
}

const f = new F('F')
console.log(f)  // F { name: 'F' }

new 命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示是函数调用,推荐使用括号。为了避免不必要的麻烦,最好加上括号。

// 推荐的写法
const f = new F()
// 不推荐的写法
const f = new F

如果忘了使用 new 命令,直接调用,构造函数就变成了普通函数,并不会生成实例对象。

function F (name) {
  this.name = name
}

const f = F('F')
console.log(f)  // undefined

为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。

function F (name) {
  'use strict'
  this.name = name
}

const f = F('F')
// TypeError: Cannot set properties of undefined (setting 'name')

为了程序按照预期执行,另一个解决办法,构造函数内部判断是否使用 new 命令,如果发现没有使用,则直接返回一个实例对象。

function F(name) {
  this.name = name

  return this instanceof F ? this : new F(name)
}

const f1 = F('F1')
console.log(f1)  // F { name: 'F1' }

const f2 = new F('F2')
console.log(f2)  // F { name: 'F2' }

new命令的原理

使用 new 命令时,它后面的函数依次执行下面的步骤。

  1. 创建一个空的简单 JavaScript 对象(即 {}),作为将要返回的对象实例。
  2. 为步骤 1 新创建的对象添加属性 __proto__,将该属性链接至构造函数的原型对象 prototype
  3. 将步骤 1 新创建的对象作为 this 的上下文。
  4. 如果该函数没有返回对象,则返回 this

也就是说,构造函数内部,this 指的是一个新生成的空对象,所有针对 this 的操作,都会发生在这个空对象上。

如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象。

function F(name) {
  this.name = name
  return name
}

// 传入字符串
let name = 'F'
const f1 = new F(name)
console.log(f1, f1 === name)  // F { name: 'F' }, false

上面的代码中,构造函数 Freturn 返回传入的参数。当传入参数为字符串 Fnew 命令返回构造后的 this 对象。但是,如果传入的参数是一个与 this 无关的新对象,new 命令就直接返回这个新对象,不会再返回 this 对象。

// 传入对象
name = { name: 'F' }
const f2 = new F(name)
console.log(f2, f2 === name) // { name: 'F' }, true

另一方面,如果对普通函数(内部没有 this 关键字的函数)使用new 命令,则会返回一个空对象。

function F() {
  return 'F'
}

const f = new F()
console.log(f, typeof f)  // F {}, object

new 命令简化的内部流程,可以用下面的代码表示。

function make (ctor, ...args) {
  // 创建空对象,继承构造函数原型
  const context = Object.create(ctor.prototype)

  // 绑定this,执行构造函数
  const result = ctor.apply(context, args)

  // 执行结果是对象,直接返回,否则返回 context
  return (typeof result === 'object' && result != null) ? result : context
}

function F(name) {
  this.name = name
}

const f = make(F, 'F')
console.log(f)  // F { name: 'F' }

new.target

函数内部可以使用 new.target 属性。如果当前函数是 new 命令调用,new.target 指向当前函数,否则为 undefined

function F() {
  console.log(new.target)
}

F()  // undefined
new F()  // [Function: F]

因此,通过 new.target 可以判断函数调用时是否使用 new 命令。

function F() {
  if (!new.target) {
    throw new Error('请使用 new 命令调用')
  }
}

F()  // Error: 请使用 new 命令调用