上一篇啊,我们聊了聊字面量对象和自定义构造函数。这一篇,我们继续,来聊聊new和数组字面量。
三、强制使用new的模式
要知道,构造函数,只是一个普通的函数,只不过它却是以new的方式调用。如果在调用构造函数时忘记制定new操作符会发生什么?这并不会导致语法或运行时错误,但可能导致逻辑错误或意外的行为发生。发生这类问题是因为您忘记使用new操作符,从而导致结构函数中的this指向了全局对象(在浏览器中,this会指向window)。
// 构造函数 function Waffle() { this.tastes = 'yummy'; } // 定义一个新对象 var good_morning = new Waffle(); console.log(typeof good_morning); // "object" console.log(good_morning.tastes); //"yummy" // 反模式 // 忘记使用new操作符 var good_morning_1 = Waffle(); console.log(typeof good_morning_1); // "undefined" console.log(window.tastes); //"yummy" console.log(good_morning_1.tastes); //"Error"
上面的代码,在没有使用new操作符的情况下,没有改变this的指向,导致污染了全局,并得到了不符合预期的结果。
命名约定
最简单的方法是使用命名约定,使构造函数的名称中的首字母变成大写(MyConstructor),并且使“普通”函数和方法的名称中的首字母变成小写(MyFunction)。
使用that
遵循命名约定一定程度上有助于避免忘记使用new所带来的问题,但是命名约定只是一种建议,不具有强制保证正确的行为。下面的模式可以确保构造函数的行为总是表现出应有的预期。
// 构造函数 function Waffle() { var that = {}; that.tastes = 'yummy'; return that; } // 对于简单的对象,甚至不需要类似that这样的局部变量 function Waffle1() { return { tastes:"yummy" }; } // 使用上面任何一种Waffle()的实现方式都总是会返回一个对象,而无论它是如何被调用的: var first = new Waffle(), second = Waffle(); console.log(first.tastes); console.log(second.tastes);
需要注意的是,上面代码中的that,只是一个命名公约,你可以使用任意名称。
但是,上面的解决方法有个问题,就是会丢失原型的连接,因为,您添加到Waffle()原型的成员,对于对象来说都是不可用的。我们看代码:
// 构造函数 function Waffle() { this.tastes = 'yummy'; } var first = new Waffle(); console.log(first.tastes); Waffle.prototype.getName = function (){console.log(this.tastes + '------')}; first.getName();
这是正常的模式,构造函数中隐式的返回this,并且我们可以获取到原型链上的方法。但是:
// 构造函数 function Waffle() { var that = {}; that.tastes = 'yummy'; return that; } // 使用上面任何一种Waffle()的实现方式都总是会返回一个对象,而无论它是如何被调用的: var first = new Waffle(), second = Waffle(); console.log(first.tastes); console.log(second.tastes); Waffle.prototype.getName = function (){console.log(this.tastes + '------')} first.getName() second.getName()
好吧,实际上我只是在最开始的代码里,给构造函数的原型上加了个方法,我们发现,无论是first.getName()还是second.getName()都会报错。这是为什么呢?区别就在于,你在构造函数内部返回的是的对象,是否继承了构造函数本身的原型链。
那么,还是上面的代码,我把this赋值给that是不是就可以了?
function Waffle() { //注意这里细微的区别 var that = this; that.tastes = 'yummy'; return that; } // 使用上面任何一种Waffle()的实现方式都总是会返回一个对象,而无论它是如何被调用的: var first = new Waffle(), second = Waffle(); console.log(first.tastes); console.log(second.tastes); Waffle.prototype.getName = function (){console.log(this.tastes + '------')} first.getName() second.getName()
但是,我们发现,second.getName()报错了。这是因为没有new运算符所做的内部逻辑,前面的章节说过。new操作符到底做了什么:创建一个空对象并且this变量引用了该对象,同时还继承了该函数的原型。这是最重要的一句,所以,你没有用new,没有继承该函数的原型。那你说我自己手动继承行不行。当然可以,这里我就不演示了,自己去尝试一下。我们继续。
自调用构造函数
为了解决前面模式的缺点,并使得原型(prototype)属性可在实例对象中使用,那么可以考虑下面的方法。具体来说,可以在构造函数中检查this是否为构造函数的一个实例,如果为否,构造函数可以再次调用自身,并且在这次调用中正确地使用new操作符:
// 构造函数 function Waffle() { if(!(this instanceof Waffle)){ return new Waffle(); } this.tastes = 'yummy'; } Waffle.prototype.getName = function (){console.log(this.tastes + '------')} var first = new Waffle(), second = Waffle(); console.log(first.tastes); console.log(second.tastes); first.getName() second.getName()
实际上,上面的代码就是判断生成的实例是否是由该构造函数所创建的,如果不是,就重新通过new运算符创建一下。
另一种用于检测实力对象的通用方法是将其与arguments.callee进行比较,而不是在代码中硬编码构造函数名称:
if(!(this instanceof arguments.callee)){ return new arguments.callee(); }
使用这种模式是基于这样一个事实:即在每个函数内部,当该函数被调用时,将会创建一个名为arguments的对象,其中包含了传递给该函数的所有参数。同时,arguments对象中又一个名为callee的属性,该属性会指向被调用的函数。
需要注意的是,在ES5的严格模式中,并不支持arguments.callee属性,因此,最好限制在将来才使用该属性。
四、数组字面量
JavaScript中的数组与语言中的绝大多数事物比较相似,即都是对象。当然,数组也同样可以通过内置的构造函数Array()来创建,但是极其不推荐这种做法。请使用数组字面量来创建一个数组!
// 具有三个元素的数组 // 反模式 var a = new Array("itsy","bitsy","spider"); // 完全相同的数组 var a = ["itsy","bitsy","spider"]; console.log(typeof a); //输出“object”,这是由于数组本身也是对象类型 console.log(a.constructor === Array); //输出true
数组字面量语法
数组字面量表示法并没有太多的内容:它只是一个逗号分隔的元素列表,并且整个列表包装在方括号中。可以给数组元素制定任意类型的值,包括对象或者其它数组。
数组字面量语法是非常简单、明确,并且优美的。毕竟,一个数组仅是一个零值缩阴列表。为此,也没有必要通过引入构造函数以及使用new操作符使得事情变得复杂。
数组构造函数的特殊性
避开new Array()的另一个理由是用于避免构造函数中可能产生的陷阱。
// 具有一个元素的数组 var a = [3]; console.log(a.length); // 1 console.log(a[0]); // 3 // 具有三个元素的数组 var a = new Array(3); console.log(a.length); // 3 console.log(typeof a[0]); // "undefined"
上面的例子,当向数组构造函数传递一个整数时,它并不会作为数组的第一个值。相反,它却设定了数组的长度。这意味着new Array(3)这个语句创建了一个长度为3的数组,但是该数组中并没有实际的元素。
假如,你像数组构造函数中传递了一个浮点数,那么情况会变得更糟:
// 具有一个元素的数组 var a = [3.14]; console.log(a[0]); // 3.14 // 具有三个元素的数组 var a = new Array(3.14); console.log(typeof a); // "undefined"
为了避免您在运行时创建动态数组可能产生的潜在错误,坚持使用数组字面量表示法。程序将会更加安全。
tips:虽然有一些使用Array()构造函数的灵巧方法,比如重复字符串。下面的代码片段返回了一个具有255个空白字符的字符串(为什么不是256个呢?)。
var white = new Array(256).join(' ');
检查数组性质
以数组作为操作数并且使用typeof操作符,其结果会返回“object”。
虽然这种行为是有意义的(数组也是对象),但对于排除错误却没有什么帮助。通常,需要知道某个值是否是一个数组。有时候,可以检查代码是否存在length属性或者一些数组方法,比如slice()方法,以此来确定该值是否具有“数组性质”。
但是这些检查机制并不健壮,因为没有任何理由确定一个非数组对象就不能具有同样名称的属性和方法。另外一些人使用instanceof Array进行检查,但是这种检查机制在某些IE浏览器版本中的不同框架中运行并不正确。
ECMAScript 5定义了一个新方法,Array.isArray(),该函数在参数为数组时返回true:
console.log(Array.isArray([]));// true // 试图以一个类似数组的对象欺骗检查 console.log(Array.isArray({ length:1, "0":1, slice:function() {} }));// false
但是,假如,在您的环境中无法使用这种新方法,可以通过调用Object.prototype.toString()方法对其进行检查。如果在数组上、下文中调用了toString的call()方法,他应该返回字符串“[object Array]”。如果该上、下文是一个对象,则它应该返回字符串“[object Object]”。因此,可以这样:
if(typeof Array.isArray === 'undefined'){ Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === "[object Array]"; }; }
本来,想要把JSON的部分也写在这里,但是这篇的内容和重点已经足够多了。还是放在下一篇文章吧。