您现在的位置是:网站首页 > 构造函数与new操作符文章详情

构造函数与new操作符

构造函数与new操作符

构造函数是JavaScript中用于创建对象的特殊函数,通常与new操作符配合使用。通过构造函数可以批量创建具有相同属性和方法的对象实例,这是实现面向对象编程的重要方式之一。

构造函数的基本概念

构造函数本质上就是普通函数,但按照约定,构造函数的名称通常以大写字母开头。当使用new操作符调用函数时,该函数就作为构造函数执行:

function Person(name, age) {
  this.name = name
  this.age = age
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}`)
  }
}

const person1 = new Person('Alice', 25)
person1.sayHello() // 输出: Hello, my name is Alice

构造函数内部使用this关键字来引用新创建的对象实例。当使用new调用时,JavaScript引擎会执行以下步骤:

  1. 创建一个新的空对象
  2. 将这个新对象的原型指向构造函数的prototype属性
  3. 将构造函数的this绑定到这个新对象
  4. 执行构造函数内部的代码
  5. 如果构造函数没有显式返回对象,则返回这个新对象

new操作符的工作原理

new操作符在JavaScript中扮演着关键角色。理解它的内部机制有助于更好地使用构造函数:

function myNew(constructor, ...args) {
  // 1. 创建一个新对象,并将其原型指向构造函数的prototype
  const obj = Object.create(constructor.prototype)
  
  // 2. 调用构造函数,将this绑定到新对象
  const result = constructor.apply(obj, args)
  
  // 3. 如果构造函数返回了一个对象,则返回该对象;否则返回新创建的对象
  return result instanceof Object ? result : obj
}

// 使用自定义的new实现
const person2 = myNew(Person, 'Bob', 30)
console.log(person2.name) // 输出: Bob

这个自定义的myNew函数模拟了原生new操作符的行为。在实际开发中,我们通常直接使用new,但了解其原理非常重要。

构造函数的返回值

构造函数通常不需要显式返回值,但如果返回了值,会影响new操作的结果:

function Car(model) {
  this.model = model
  // 返回原始值会被忽略
  return 'This will be ignored'
}

const car1 = new Car('Tesla')
console.log(car1.model) // 输出: Tesla

function Bike(model) {
  this.model = model
  // 返回对象会替代新创建的对象
  return { custom: 'object' }
}

const bike1 = new Bike('Yamaha')
console.log(bike1.model) // undefined
console.log(bike1.custom) // 输出: object

构造函数与原型

构造函数与原型对象紧密相关。通过原型,我们可以实现方法的共享,避免每个实例都创建自己的方法副本:

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

// 在原型上添加方法
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise`)
}

const animal1 = new Animal('Dog')
const animal2 = new Animal('Cat')

// 两个实例共享同一个speak方法
console.log(animal1.speak === animal2.speak) // true

构造函数继承

JavaScript使用原型链实现继承。子构造函数可以通过调用父构造函数并设置正确的原型链来实现继承:

function Vehicle(type) {
  this.type = type
}

Vehicle.prototype.drive = function() {
  console.log(`Driving a ${this.type}`)
}

function Car(type, brand) {
  Vehicle.call(this, type) // 调用父构造函数
  this.brand = brand
}

// 设置原型链
Car.prototype = Object.create(Vehicle.prototype)
Car.prototype.constructor = Car

Car.prototype.honk = function() {
  console.log(`${this.brand} honks!`)
}

const myCar = new Car('sedan', 'Toyota')
myCar.drive() // 输出: Driving a sedan
myCar.honk() // 输出: Toyota honks!

ES6类与构造函数

ES6引入了class语法糖,它底层仍然是基于构造函数和原型的:

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  
  greet() {
    console.log(`Hello, I'm ${this.name}`)
  }
}

const person = new Person('Charlie', 28)
person.greet() // 输出: Hello, I'm Charlie

// 类本质上仍然是函数
console.log(typeof Person) // function

构造函数的安全检查

为了防止构造函数被错误调用(没有使用new),可以在构造函数内部添加检查:

function User(name) {
  // 确保通过new调用
  if (!(this instanceof User)) {
    return new User(name)
  }
  this.name = name
}

const user1 = User('Alice') // 即使忘记new也能正常工作
const user2 = new User('Bob')

console.log(user1.name) // Alice
console.log(user2.name) // Bob

在ES6中,可以使用new.target来检测:

function Product(name) {
  if (!new.target) {
    throw new Error('Product must be called with new')
  }
  this.name = name
}

try {
  const p = Product('Widget') // 抛出错误
} catch(e) {
  console.error(e.message) // Product must be called with new
}

构造函数的应用场景

构造函数常用于创建特定类型的多个对象。例如,在游戏开发中创建多个敌人实例:

function Enemy(type, health, attack) {
  this.type = type
  this.health = health
  this.attack = attack
  this.isAlive = true
}

Enemy.prototype.takeDamage = function(damage) {
  this.health -= damage
  if (this.health <= 0) {
    this.isAlive = false
    console.log(`${this.type} has been defeated!`)
  } else {
    console.log(`${this.type} has ${this.health} health remaining`)
  }
}

const orc = new Enemy('Orc', 100, 15)
const goblin = new Enemy('Goblin', 40, 8)

orc.takeDamage(30) // 输出: Orc has 70 health remaining
goblin.takeDamage(50) // 输出: Goblin has been defeated!

构造函数与工厂函数对比

除了构造函数,工厂函数也是创建对象的常用方式。两者各有优缺点:

// 构造函数方式
function Circle(radius) {
  this.radius = radius
}

Circle.prototype.area = function() {
  return Math.PI * this.radius ** 2
}

// 工厂函数方式
function createCircle(radius) {
  return {
    radius,
    area() {
      return Math.PI * radius ** 2
    }
  }
}

const c1 = new Circle(5)
const c2 = createCircle(5)

console.log(c1.area()) // 78.53981633974483
console.log(c2.area()) // 78.53981633974483

// 主要区别
console.log(c1 instanceof Circle) // true
console.log(c2 instanceof Object) // true

构造函数的性能考虑

由于构造函数会在每个实例上创建方法副本,将方法定义在原型上更高效:

// 不推荐 - 每个实例都有自己的方法副本
function InefficientWidget(name) {
  this.name = name
  this.logName = function() {
    console.log(this.name)
  }
}

// 推荐 - 方法在原型上共享
function EfficientWidget(name) {
  this.name = name
}

EfficientWidget.prototype.logName = function() {
  console.log(this.name)
}

const widgets1 = []
const widgets2 = []

// 测试创建10000个实例
console.time('Inefficient')
for (let i = 0; i < 10000; i++) {
  widgets1.push(new InefficientWidget('Widget' + i))
}
console.timeEnd('Inefficient') // 耗时更长

console.time('Efficient')
for (let i = 0; i < 10000; i++) {
  widgets2.push(new EfficientWidget('Widget' + i))
}
console.timeEnd('Efficient') // 耗时更短

构造函数与对象字面量

对于简单对象,直接使用对象字面量可能更合适:

// 使用构造函数
function Book(title, author) {
  this.title = title
  this.author = author
}

const book1 = new Book('JavaScript', 'John Doe')

// 使用对象字面量
const book2 = {
  title: 'JavaScript',
  author: 'John Doe'
}

// 当需要创建多个相似对象时,构造函数更有优势
const books = []
for (let i = 0; i < 10; i++) {
  books.push(new Book(`Book ${i}`, `Author ${i}`))
}

构造函数的进阶应用

构造函数可以与其他模式结合使用,如模块模式:

const Counter = (function() {
  // 私有变量
  let privateCounter = 0
  
  // 构造函数
  function Counter() {
    this.increment = function() {
      privateCounter++
      console.log(privateCounter)
    }
    this.decrement = function() {
      privateCounter--
      console.log(privateCounter)
    }
  }
  
  return Counter
})()

const counter1 = new Counter()
const counter2 = new Counter()

counter1.increment() // 1
counter1.increment() // 2
counter2.decrement() // 1 - 共享同一个privateCounter

构造函数与this绑定

构造函数中的this绑定有时会导致意外行为,特别是在回调函数中:

function Timer() {
  this.seconds = 0
  setInterval(function() {
    // 这里的this指向全局对象或undefined(严格模式)
    this.seconds++ // 不会按预期工作
    console.log(this.seconds) // NaN
  }, 1000)
}

const timer = new Timer()

// 解决方案1: 使用箭头函数
function FixedTimer1() {
  this.seconds = 0
  setInterval(() => {
    this.seconds++ // 正确绑定
    console.log(this.seconds)
  }, 1000)
}

// 解决方案2: 保存this引用
function FixedTimer2() {
  this.seconds = 0
  const self = this
  setInterval(function() {
    self.seconds++ // 通过闭包访问
    console.log(self.seconds)
  }, 1000)
}

构造函数的静态方法

可以在构造函数上直接定义静态方法,这些方法属于构造函数本身而非实例:

function MathUtils() {}

// 静态方法
MathUtils.add = function(a, b) {
  return a + b
}

// 实例方法
MathUtils.prototype.multiply = function(a, b) {
  return a * b
}

console.log(MathUtils.add(2, 3)) // 5 - 直接通过构造函数调用

const util = new MathUtils()
console.log(util.multiply(2, 3)) // 6 - 通过实例调用
// console.log(util.add(2, 3)) // 错误 - 实例不能访问静态方法

在ES6类中,静态方法使用static关键字定义:

class StringUtils {
  static reverse(str) {
    return str.split('').reverse().join('')
  }
  
  toUpperCase(str) {
    return str.toUpperCase()
  }
}

console.log(StringUtils.reverse('hello')) // olleh
const utils = new StringUtils()
console.log(utils.toUpperCase('hello')) // HELLO

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步