构造函数

162 阅读5分钟

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

前言

我们之前提到过, ECMAScript 中的构造函数是用于创建特定类型对象的。今天我们就来谈一谈构造函数。像 Object 和 Array 一样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,函数自己的对象类型定义属性和方法。 比如,前面的例子使用构造函数模式可以这样写: 

function Person ( name , ag , job ){
 thiS . name = name ; thiS . age = age ; this . job = job ;
 thiS . SayName = function (){ console . log ( thiS . name );
}

 let personl = new Person (" Nicholas ",29," Software Engineer "); 
 let person2= new Person (" Greg ",27," Doctor ");
 personl . sayName ();
 Nicholas person2. sayName ();// Greg 

在这个例子中, Person ()构造函数代替了 createPerson ()工厂函数。实际上, Person (内部的代码跟 createPerson ()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。 
  • 属性和方法直接赋值给了 this 。
  • 没有 returno  另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是能创建对象的函数。

构造函数也是函数

构造凼效与背通的数唯一的区别就是调用方式不局。除此→外,构浩函数也是函数。并没有把来函数定义为构造的效的特殊语法。任何函数只要伸用 nev 操作符调用就是构造函数,而不使用 new 坏作符调用的函数就是晋通函数。比如,前面的例子中定义的 PersonO )可以像下面这样调用:\

//作为构造函数
1et person = new Person (" Nicholas ",29," Software Engineer ");
 person . sayName (); /7" Nicholas "
//作为函数调用
 Person (" Greg ",27," Doctor ");//添加到 window 对象 
 window . sayName ();//" Greg "
//在另一个对象的作用域中调用 let o = new Object ();
 Person .cal1( o ," Kristen ",25,“ Nurse ");
 o . sayName ();//" Kristen "

这个例子一开始展示了典型的构造函数调用方式,即使用 new 操作符创建一个新对象。然后是普通函数的调用方式,这时候没有使用 new 操作符调用Person0),结果会将属性和方法添加到 window 对象。这里要记住,在调用一个函数而没有明确设置 this 值的情况下(即没有作为对象的方法调用,或者没有使用call0/apply0)调用), this 始终指向 Global 对象(在浏览器中就是 window 对象)。因此在上面的调用之后, window 对象上就有了一个 sayName ()方法,调用它会返回" Greg "。最后展示的调用方式是通过call0)(或apply0)调用函数,同时将特定对象指定为作用域。这里的调用将对象。指定为 Person ()内部的 this 值,因此执行完函数代码后,所有属性和 sayName ()方法都会添加到对象 o 上面

构造函数的问题

构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言, personl 和person2都有名为 sayName (的方法,但这两个方法不是同一个 Function 实例。我们知道, ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数实际上是这样的:  

function Person ( name , age , job ){ this . name = name ;
 this . age = age ; this . job = job ;
 this . sayName = newFunction (" con В ole . log ( thi В. name )");/逻辑等价

这样理解这个构造函数可以更清楚地知道,每个 Person 实例都会有自己的 Function 实例用于显示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function 实例的机制是一样的。因此不同实例上的函数虽然同名却不相等,如下所示:  console . log (person1. sayName ==person2.sayName);// false

因为都是做一样的事,所以没必要定义两个不同的 Function 实例。况且, this 对象可以把函数与对象的绑定推迟到运行时。 要解决这个问题,可以把函数定义转移到构造函数外部: Pegoateot 

 function Person ( name , age , job ){
 this . name = name ;
 this . age = age 
 this . job = job ;
 this .ВayName =ВayName 
 function В ayName (){ 
 con В ole . log ( thi В. name );
 let person1= new Person (" Nicholas ",29," Software Engineer ");
 let person2= new Person (" Greg ",27," Doctor ");
person1. sayName ();// Nicholas 
person2. sayName ();// Greg 

在这里, sayName ()被定义在了构造函数外部。在构造函数内部, sayName 属性等于全局 sayName ()函数。因为这一次 sayName 属性中包含的只是一个指向外部函数的指针,所以person1和person2共享了定义在全局作用域上的 sayName ()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。