什么是构造函数
- 在 JavaScript 中,用new关键字来调用的函数,称为构造函数。构造函数首字母一般大写(规范)
- 构造函数不是在函数定义时决定的,而是在函数调用时,取决于它的调用方式
- 除箭头函数和IIFE外,任何函数都有可能成为构造函数
构造函数和普通函数的区别
- 构造函数首字母大小区别与普通函数(命名方式)
- 构造函数使用 new 关键字调用,普通函数通过直接调用函数执行(调用方式)
- 构造函数用于创建新的实例对象,普通函数用来执行对应的函数功能(用途)
- 构造函数中的 this 指向每一个对应的实例对象,普通函数的 this 由谁调用便指向谁,除非人为使用 call/apply/bind 改变 this 指向(this 指向)
为什么需要构造函数
- 将具有相同属性或功能的事物,抽象成为一个类,实现自定义类型
- 减少无用的代码量,提高代码质量
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 的实例
p1.proto === Person: true | p1 的原型指向其构造函数的原型,即 p1 的原型链上出现了 Person 的构造函数
构造函数注意事项
- 1.若构造函数有返回值且返回值是一个对象类型,则返回该对象,且该实例的类型为 Object
- 2.若构造函数没有使用 this 为实例指定属性,则返回一个空的构造函数的实例对象
- 2.构造函数中的代码会逐行执行,有可能会改变外部数据
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)
- 实现 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()
- 每一次创建实例对象时,都会复制一份属性和方法,而每一个实例对象的属性都是不同的,没有什么问题。但是执行的方法都是同样的代码,具有相同的功能,没有必要复制。每一个实例对象中各自存在相同的方法会浪费内存
- 解决方式一: 定义全局方法
//定义全局方法
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()
- 定义全局函数的缺点:全局函数非常容易被覆盖,污染全局变量
- 解决方式二: 在构造函数的原型对象上添加公共方法
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 方法
为什么箭头函数不能作为构造函数
- 构造函数是通过 new 关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定 this 的过程,而箭头函数没有自己的 this。创建对象过程,new 首先会创建一个空对象,并将这个空对象的proto指向构造函数的 prototype,从而继承原型上的方法,但是箭头函数没有 prototype。因此不能使用箭头作为构造函数,也就不能通过 new 操作符来调用箭头函数