构造函数和new关键字

307 阅读6分钟

什么是构造函数

  • 在 JavaScript 中,用new关键字来调用的函数,称为构造函数。构造函数首字母一般大写(规范)
  • 构造函数不是在函数定义时决定的,而是在函数调用时,取决于它的调用方式
  • 除箭头函数和IIFE外,任何函数都有可能成为构造函数

构造函数和普通函数的区别

  1. 构造函数首字母大小区别与普通函数(命名方式)
  2. 构造函数使用 new 关键字调用,普通函数通过直接调用函数执行(调用方式)
  3. 构造函数用于创建新的实例对象,普通函数用来执行对应的函数功能(用途)
  4. 构造函数中的 this 指向每一个对应的实例对象,普通函数的 this 由谁调用便指向谁,除非人为使用 call/apply/bind 改变 this 指向(this 指向)

为什么需要构造函数

  1. 将具有相同属性或功能的事物,抽象成为一个类,实现自定义类型
  2. 减少无用的代码量,提高代码质量

a instanceof b 的原理

查看 b 的 prototype 指向的对象是否在 a 的原型链上

创建对象方式对比

  • 1.普通方式定义多个具有相同属性的对象
  const obj1 = {name: '奔波儿灞',age: 18}
  const obj2 = {name: '灞波儿奔',age: 9}
  const obj3 = {name: '灵感大王',age: 6}
  • 2.函数工厂模式定义多个具有相同属性的对象
  function createPerson(name,age) {
    return {name,age}
  }
  const p1 = createPerson("奔波儿灞",18)
  const p2 = createPerson("灞波儿奔",9)
  const p3 = createPerson("灵感大王",6)

虽然可以使用工厂函数可以创建多个对象,但是对象无法区别是哪一种类型,全部都继承于 Object 类型

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const p1 = new Person("奔波儿灞", 18);
const p2 = new Person("灞波儿奔", 9);
const p3 = new Person("灵感大王", 6);

p1 instanceof Person: true | p1 是 Person 的实例

a1.png p1.proto === Person: true | p1 的原型指向其构造函数的原型,即 p1 的原型链上出现了 Person 的构造函数

a2.png

构造函数注意事项

  • 1.若构造函数有返回值且返回值是一个对象类型,则返回该对象,且该实例的类型为 Object

a4.png

a3.png

  • 2.若构造函数没有使用 this 为实例指定属性,则返回一个空的构造函数的实例对象

a5.png

  • 2.构造函数中的代码会逐行执行,有可能会改变外部数据

a6.png

new 关键字做了什么

  • 1.立即创建一个对象
  • 2.将新建的对象原型指向构造函数的原型**
  • 3.执行构造函数,将新建的对象绑定构造函数中的 this,替代构造函数中的上下文
  • 4.判断构造函数的返回值,如果是一个 Object 类型,则返回构造函数的返回值,否则返回新建的对象

实现自己的 new 方法

  function myNew(constructor,...rest) {
    //创建新的对象
    const obj = {}
    //对象的原型指向构造函数的原型
    Object.setPrototypeOf(obj,constructor.prototype)
    //执行构造函数,并将新建的对象绑定到构造函数的`this`上
    const context = constructor.apply(obj,rest)
    //判断返回值是不是对象类型
    return context instanceof Object ? context : obj
  }
  //构造函数1Person
  function Person(name,age) {
    this.name = name
    this.age = age
  }
  //构造函数二 函数返回一个对象
  function Animal(name,age) {
    this.name = name
    this.age = age
    return {name:'默认对象',age: '默认年龄'}
  }
  const p1 = myNew(Person,'奔波儿灞',19)
  const p2 = new Person('奔波儿灞',19)
  const a1 = myNew(Animal,'灵感大王',9)
  const a2 = new Animal('灵感大王',9)

a7.png

  • 实现 new 函数也可以有其他方式,但是总体思路不变
  //方式二
  function myNew(constructor, ...rest) {
    //根据构造函数的原型 创建新的对象
    const obj = Object.create(constructor.prototype);
    //执行构造函数,并将新建的对象绑定到构造函数的`this`上
    const context = constructor.apply(obj, rest);
    //判断返回值是不是对象类型
    return context instanceof Object ? context : obj;
  }
  //方式二
  function myNew(constructor, ...rest) {
    //创建新的对象
    const obj = {}
    //将对象原型手动绑定到构造函数到原型链
    obj.__proto__ = constructor.prototype
    //执行构造函数,并将新建的对象绑定到构造函数的`this`上
    const context = constructor.apply(obj, rest);
    //判断返回值是不是对象类型
    return context instanceof Object ? context : obj;
  }

构造函数的缺点

  function Person(name,age) {
    this.name = name
    this.age = age
    //在每一个实例中,该方法是公用的
    this.sayHellow = function () {
      console.log(this.name);
    }
  }
  const p1 = new Person("奔波儿灞",18)
  const p2 = new Person("灵感大王",88)
  p1.sayHellow()
  p2.sayHellow()
  • 每一次创建实例对象时,都会复制一份属性和方法,而每一个实例对象的属性都是不同的,没有什么问题。但是执行的方法都是同样的代码,具有相同的功能,没有必要复制。每一个实例对象中各自存在相同的方法会浪费内存
  1. 解决方式一: 定义全局方法
  //定义全局方法
  function sayHellow() {
    console.log(this.name);
  }
  function Person(name,age) {
    this.name = name
    this.age = age
    this.sayHellow = sayHellow
  }
  const p1 = new Person("奔波儿灞",18)
  const p2 = new Person("灵感大王",88)
  p1.sayHellow()
  p2.sayHellow()

  • 定义全局函数的缺点:全局函数非常容易被覆盖,污染全局变量
  1. 解决方式二: 在构造函数的原型对象上添加公共方法
  function Person(name,age) {
    this.name = name
    this.age = age
  }
  Person.prototype.sayHellow = function () {
    console.log(this.name);
  }
  const p1 = new Person("奔波儿灞",18)
  const p2 = new Person("灵感大王",88)
  p1.sayHellow()
  p2.sayHellow()

  • 因为根据 new 关键字调用构造函数,会返回构造函数的实例对象。并且该实例对象的原型指向构造函数的原型对象,因此我们就在实例的原型链上添加了构造函数的原型。在调用实例对象的方法时,如果实例对象不具有这个方法(或者属性),则会沿着原型链向上查找。如果我们在构造函数的原型上添加方法,那么实例的原型链上也具有该方法,所以会被实例对象调用

  • 我们可以看到实例对象本身不具 sayHellow 方法,而是在其原型链上存在 sayHellow 方法

a8.png

为什么箭头函数不能作为构造函数

  • 构造函数是通过 new 关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定 this 的过程,而箭头函数没有自己的 this。创建对象过程,new 首先会创建一个空对象,并将这个空对象的proto指向构造函数的 prototype,从而继承原型上的方法,但是箭头函数没有 prototype。因此不能使用箭头作为构造函数,也就不能通过 new 操作符来调用箭头函数