ECMAScript中的构造函数是用于创建特定类型对象的。像Object和Array这样的原生构造函数,运行时可以在运行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
1.使用工厂模式:
function createPerson(name,age,job){
    let o=new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
        console.log(this.name);
    }
    return o;
}
let person1=createPerson('张三',23,'Web前端开发');
let person2=createPerson('李四',20,'IOS开发');
这里,函数 createPerson() 接受三个参数,根据这几个参数构建一个包含 Person 信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。这中工厂模式虽然可以解决创建多个类似对象的问题,但是没有解决对象标识问题(即新创建的对象是什么类型)。

2.还是上面的例子,使用构造函数:

 function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=function(){
        console.log(this.name);
    }
 }
 let person1=new Person('张三',23,'Web前端开发');
 let person2=new Person('李四',20,'IOS开发');

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

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

4.要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

1.在内存中创建一个新对象。
2.这个新对象内部 [[Prototype]] 特性被赋值为构造函数的 prototype 属性。
3.构造函数内部 this 被赋值为这个新对象 (即 this 指向新对象)。
4.执行构造函数内部的代码(给新对象添加属性)。
5.如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

5.上一个例子的最后,person1和person2分别保存着 Person 的不同实例。这两个对象都有一个 constrctor 属性指向 Person,如下所示:

console.log(person1.constructor===Person); // true
console.log(person2.constructor===Person); //true

6.constructor本来是用于标识对象类型的。不过,一般认为 instanceof 操作符是确定对象类型更可靠的方式。前面例子中每个对象都是 Object的实例,同时也是 Person 的实例,如下面调用 instanceof 操作符的结果所示:

console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(person2 instanceof Person);
console.log(person2 instanceof Object);  // true
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在这里例子中,person1和person2之所以也会被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。

7.构造函数不一定携程函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:

let Person=function(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=function(){
        console.log(this.name);
    };
}

let person1=new Person('张三',23,'Web前端开发');
let person2=new Person('李四',20,'IOS开发');

person1.sayName(); // 张三 
person2.sayName(); // 李四

console.log(person1 instanceof Object);// true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person);// true

8.在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有new操作符,就可以调用相应的构造函数:

function Person(){
    this.name='陈';
    this.sayName=function(){
        console.log(this.name);
    }
}
let person1=new Person();
let person2=new Person();
person1.sayName();// 陈
person2.sayName();// 陈
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person);//true

9.构造函数也是函数。构造函数与普通函数唯一区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符就是构造函数,而不使用 new操作符调用的函数就是普通函数。比如,前面的例子中定义的 Person()可以像下面这样调用:

//作为构造函数
let person=new Person('张三',23,'Web前端开发');
person.sayName();// 张三

//作为函数调用
Person('陈',23,'Web前端开发');
window.sayName(); // '陈'

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

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

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=new Function("console.log(this.name)");  //逻辑等价
}

11.这样理解这个狗构造函数可以更清楚地知道,每个 Person实例都会有自己的 Function 实例用于显示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和表示解析。但创建新 Function 实例的机制时一样的。因此不同实例上的函数虽然同名却不相等,如下所示:

console.log(person1.sayName===person2.sayName);//false

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

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=sayName;
}
function sayName(){
    console.log(this.name);
}
let person1=new Person("张三",23,"Web前端开发");
let person2=new Person("李四",20,"IOS开发");
在这里,sayName()被定义在了构造函数外部。子啊构造函数内容,sayName属性等于全局sayName()函数。因为这一次 sayName属性中包含的只是一个指向外部函数的指针,所以person1和person2共享了定义在全局作用域上的sayName()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际时上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

13.本期的分享到了这里就结束啦,希望对你有所帮助,让我们一起努力走向巅峰!


灰太狼的情与殇
169 声望7 粉丝

吾不是什么大佬,在这个领域我从未想赢,只是不甘认输。生活再平凡,也是限量版,让我们走出不一样的人生,活的精彩。


下一篇 »
原型模式