如何写一个作用域安全的构造函数
基础部分
构造函数本质上就是一个使用new操作符调用的函数,使用new调用时,构造函数内用到的this对象会指向新创建的对象实例:
function girlfriend(name, age, height) { this.name = name; this.age = age; this.height = height; } // 使用new操作符来分配这些属性 var girlfriend = new girlfriend("ying", 23, 170);
平时写变量时,如果因为失误忘记使用new操作符时,会造成一个糟糕的影响————因为this对象是在运行时绑定的,直接调用构造函数,this会映射到全局对象window上,导致错误对象属性的增加,增添不必要的变量到全局对象中:
var girlfriend = new girlfriend("ying", 23, 170); console.log(window.name); // "ying" console.log(window.age); // 23 console.log(window.height); // 170
特别的,当你自己构造函数内的某些变量名与window变量名重名时(像一些常用变量名name、length等),对这些属性的偶然覆盖就很可能导致其他地方出错,并且这个bug还相当难找!
在这种情况下构造一个作用域安全的构造函数就显得很有必要:
function girlfriend(name, age, height) { // 首先确认this对象是正确类型的实例, // 如果不是就创建新的实例并返回 if (this instanceof girlfriend) { // 添加一个检查 console.log('created'); this.name = name; this.age = age; this.height = height; } else { console.log('new'); return new girfriend(name, age, height); } } var girlfriend1 = girlfriend("ying", 23, 170); // "new" "created" console.log(window.name); // "" console.log(girfriend1.name); // "ying" var girlfriend2 = new girlfriend("lin", 22, 165); // "created" console.log(girfriend1.name); // "lin"
girlfriend1背后构造函数先new了一个实例并返回实例(打印“new”),再对实例进行赋值(打印“created”)。
girlfriend2自己就先new了一个实例,直接对该实例进行赋值(只打印“created”)。
这样在任何情况下就都可以返回一个安全作用域的实例了。
进阶部分
使用上面添加一个检查的方法可以创建一个作用域安全的构造函数,但如果有的函数窃取该函数的继承且没有使用原型链,那这个继承将被破坏不生效:
function bmi(sex, weight=1, height=1) { // es6开始支持的默认值 if (this instanceof bmi) { this.sex = sex; this.weight = weight; this.height = height; this.getbmi = function() { return this.weight / (this.height ** 2); }; } else { return new bmi(sex); } } function people(height, weight) { bmi.call(this, 'male'); this.height = height; this.weight = weight; } var guy = new people(1.75, 68); // 单位是m和kg console.log(guy.sex) // undefined
bmi构造函数作用域是安全的,但people并不是。新创建一个people实例后,这个实例准备通过bmi.call()来继承bmi的sex属性,但由于bmi的作用域是安全的,this对象并非是bmi的实例,所以bmi会先自己创建一个新的bmi对象,不会把新的bmi对象的值传递到people中去。
这样people中的this对象并没有得到增长,同时bmi.call()返回的值也没有用到,所以people实例中就不会有sex、weight、height属性和getbmi()函数。
解决办法: 构造函数结合使用原型链或寄生组合:
function bmi(sex, weight=1, height=1) { if (this instanceof bmi) { this.sex = sex; this.weight = weight; this.height = height; this.getbmi = function() { return this.weight / (this.height ** 2); }; } else { return new bmi(sex); } } function people(height, weight) { bmi.call(this, 'male'); this.height = height; this.weight = weight; } people.prototype = new bmi(); // 重点 var guy = new people(1.75, 68); console.log(guy.sex) // "male"
这样写的话,一个people的实例同时也是一个bmi的实例,所以bmi.call()才会去执行,为people实例添加上属性和函数。
总结
当多个人一同构建一个项目时,作用域构安全函数就非常必要,对全局对象意外的更改可能就会导致一些常常难以追踪的错误,这和平常设置空变量和空函数一样避免因为其他人可能发生的错误而阻塞程序执行。