第二章 字面量与构造函数
前言
JavaScript中可以使用字面量的形式去声明对象,相比较使用构造函数去声明,更加准确且不容易出错,代码也更加简洁,这里讨论常见的类型中,为什么基于字面量的形式更加可取。
对象字面量
当我们考虑创建一个键-值对哈希表时(在其他语言被称为关联数组),我们可以从一个“空”对象开始,然后再根据需要向其添加所需要的内容,对于按需对象创建方式而言,对象字面量表示法是一种非常理想的选择方法。
var dog = {};
dog.name = "jingmao";
dog.getName = function() {
return dog.name;
}
//删除和复写也非常简单明了
delete dog.name;
dog.getName = "null";
当然,JavaScript也并不要求必须是从空对象开始,对于字面量模式可以在创建时就向其添加需要的内容
var dog = {
name: "jingmao",
getName: function() {
return dog.name;
}
}
在这里,空对象并不是指真正的空,泛指没有添加自定义属性的对象,因为在JavaScript中,哪怕是最简单的{}
对象;也会从原型继承到属性和方法。
构造函数的对象
通过构造函数也可以创建对象,在JavaScript中内置了Object
构造函数,我们可以通过它来创建一个对象。
const dog = new Object();
dog.name = "jingmao";
dog.getName = function() {
return dog.name;
}
使用该方式存在一个缺点,除了Objcet可能被复写外,相对于字面量,它存在作用域解析,它需要从作用域中找到Object
构造函数,如果层级过深,会一直沿着作用域链查询,直到找到为止。
甚至于使用new Objcet()
创建时,传入不同的值,会产生不同的结果。
var a = new Object();
console.log(a.constructor === Object); //true
var b = new Object(1);
console.log(b.constructor === Number); //true
console.log(typeof b === "number"); //true
var c = new Object("name");
console.log(c.constructor === String); //true
console.log(typeof c === "string"); //true
var d = new Object(true);
console.log(d.constructor === Boolean); //true
console.log(typeof d === "boolean"); //true
当传递给Object()
构造函数的值是动态的,并且直到运行时才能确定值的类型,这种行为可能导致意料之外的结果,因此,使用new Objct()
构造函数远远不如使用字面量带来的收益高。
自定义构造函数
除了内置的Object
构造函数,我们还可以自定义构造函数来创建对象
var a = new Person("admin");
a.say(); //admin
上面代码中,我们new了一个名为Person
的类,虽然语法上看上去和java一样,通过类去创建一个对象,但是需要知道的是,在ES5规范中,JavaScript中并没有类的概念,而ES6中的类,也仅仅是一个语法糖而已。
我们来看一下Person
的实现
function Person(name) {
this.name = name;
this.say = function() {
return this.name;
}
}
当使用new
操作符调用构造函数时,函数内部会发生以下情况:
- 创建了一个空对象,并将this指向这个新建对象,同时还继承了该函数的原型
- 执行构造函数内部代码,对新对象添加属性和方法
- 在没有显式抛出对象的情况下默认抛出this
伪代码
function Person(name) {
//新对象
var this = {};
//原型继承
this.__proto__ = Person.prototype;
//对新对象添加属性和方法,
//Person.call(this,name);
//call看不懂可以看成下面,因为new的时候,真的运行并不是函数体内,这是伪代码
this.name = name;
this.say = function() {
return this.name;
}
//判断是否有显式return,且抛出的值不是非空对象,否则使用默认的隐式抛出this
return this;
}
非空对象伪代码
function Person(name) {
this.name = name;
this.say = function() {
return this.name;
}
return ""; //空字符
}
//最终抛出this
console.log(new Person("admin")); //{ name: "admin",say: function() {} }
function Person(name) {
this.name = name;
this.say = function() {
return this.name;
}
return null; //null
}
//最终抛出this
console.log(new Person("admin")); //{ name: "admin",say: function() {} }
非空对象,return的值是无效的,最终都会抛出this。
反之我们则可以控制构造函数new时所抛出的对象。
function Person(name) {
this.name = name;
return [];
}
console.log(new Person("admin")); //[]
在本例中为了简单,直接将say方法添加到this上了,它会导致每次new Person时,都会在内存中创建一个say函数,这种方法的效率显然是非常低下的,因为多个实例之间say方法并没有改变,既然都是相同的,更好的选择就是添加到Person的原型上,通过继承的方式拿到
Person.prototype.say = function() {
return this.name;
}
强制使用new的模式
我们知道,构造函数也只是一个函数,只不过它是以new
的方式去调用,但是这并不是强制的,如果我们不使用new
去调用会发生什么?
function Person(name) {
this.name = name;
}
const a = Person("admin");
console.log(a); //undefined
console.log(window.name); //admin
直接运行构造函数时,会导致this指向window对象(在浏览器中),此时我们相当于给window添加了自定义属性,这显然并不是我们想看到的,因为程序员众所周知的一个原则就是保证全局命名空间的整洁。
这个问题也在ECMAScript5的严格模式中得到解决,在严格模式中这种用法会直接抛出错误。因为this被赋值为undefined。
"use strict";
function Person(name) {
this.name = name; //运行到这报错
}
const a = Person("admin");
console.log(a);
console.log(window.name);
构造函数的命名约定
事实上为了避免构造函数被直接作为函数使用,我们都遵循了一个命名约定,就是开头字母大写,但是这只是一种建议,并不能保证正确的行为,用户仍然可以将其作为一个普通函数来调用,这显然会带来一些问题。
自调用构造函数
为了解决这个问题,我们需要增加一些判断,我们可以在构造函数里判断this
是否为构造函数的实例,因为只有new操作符运行时,才会使得this是构造函数的实例。
function Person(name) {
//判断
if(!this instanceof Person) {
return new Person(name);
}
this.name = name;
}
当然还有另一种方式
function Person(name) {
//判断
if(!this instanceof arguments.callee) {
return new Person(name);
}
this.name = name;
}
当函数被调用时,会创建一个名为arguments
的变量,其中包含了传递给函数的参数,同时还存有被调用的函数本身,它的这个属性就是callee
;但是非常遗憾的是在严格模式下,该属性是被禁用的,因为非常危险。
数组字面量
数组的字面量非常简单,一个方括号包裹,内容之间通过逗号分割,因为数组是一个0开始的索引列表,为此没必要通过new操作符使得事情变得复杂。
数组也可以通过内置的Array
构造函数来创建,但是显然字面量的方式更加可取。
var a = [];
var b = new Array();
数组构造函数的特殊性
不推荐使用Array
构造函数的一个原因是参数带来的陷阱。当我们给构造函数传入一个数字参数是,这个参数并不会成为数组中的值,而是成为一个设定数组长度的参数。
var a = new Array(3);
console.log(a); //[,,]
console.log(a.length); //3
console.log(a[0]); //undefined
这可能并不是我们预期的结果,如果我们传入一个浮点数,那情况还能更糟糕
var a = new Array(3.14); //Uncaught RangeError: invalid array length
直接提示不合法的范围长度。
为了避免产生这种错误,使用字面量的方式会使得程序更加安全。
判断一个“数组”是否为数组
当我们使用typeof
去判断数组对象是,得到的是object
;
显然这种行为是有意义的,因为数组也是一个对象,但是对于错误排查却没有什么帮助,在ES5以前,常见的做法是通过Object.prototype.toString
方法,从上下文对象抛出一个字符串[object Array]
,此时我们可以准确的判断该变量是否为一个数组。
ES5新增了一个Array.isArray()
的方法用于判断,且支持度良好,ie9及以上和安卓全系都已支持。
var a = new Array();
console.log(Object.prototype.toString.call(a)); //[object Array]
console.log(Array.isArray(a)); //true
写一个小Polyfill
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
JSON
json它代表了JavaScript的数据传输格式,是一种轻量级的数据交换格式,且可以很方便的用于多种语言。
在json字符串中不能使用:函数、正则表达式
在ES5版本以前,JSON还没有被纳入标准,对于一个json格式的数据解析,可能往往会使用eval
来进行处理,或者使用JSON.org的第三方库,甚至一些第三方框架,都有携带对json解析的方法,比如JQ的JQuery.parseJSON()
。
显然用eval
是一个很危险的操作,如果还在旧版,可以使用第三方库,而在ES5后,可以使用内置的JSON对象进行解析。
JSON.stringify();
JSON.parse();
使用该方法除了方便以外,性能会更好,且安全性也相应高一些。
正则表达式
正则表达式也支持字面量和构造函数两种方式创建。
以匹配两个反斜杠\\
为例:
const reg1 = /\\/g;
const reg2 = new RegExp("\\\\","g");
当我们使用字面量形式创建时,除了简洁易懂外,对于反斜杠处理更加易读,当使用g全局匹配时,反斜杠虽然可以定义为转义,但是使用g之后可以视为一个普通字符。
而在构造函数时,就不得不对转义反斜杠进行再转义,导致4个反斜杠的产生,给人一种非常错乱的感觉。
当然构造函数也不是一无是处,某些场景中我们无法确定需要匹配的内容具体是什么,只能在运行时以字符串的方式创建,此时就需要构造函数了。
但是在大部分情况下,使用字面量形式的正则会更受欢迎,有助于我们编写简洁易懂的代码。
基本类型
JavaScript有5个基本类型:数字、字符串、布尔、null、undefined;除了null和undefined以外,其他三个都具有构造函数,或者我们可以换个称呼,称其为包装对象。
我们举个例子来说明其不同:
var a = 100;
console.log(a); //100
console.log(typeof a); //number
var b = new Number(100);
console.log(b); //100
console.log(typeof b); //object
可以看到,通过包装对象看上去和原来的值一模一样,但是它的类型已经变成object了,所以没有必要不要使用包装对象。
包装对象也不是一无是处,有时候我们会需要一些特殊方法,使用字面量就无法直接调起使用。
Number包装对象直接使用的方法
toFixed
toExponential
100.toFixed(); //Uncaught SyntaxError: Invalid or unexpected token 无法直接使用
const a = 100;
a.toFixed(); //100 必须创建变量
//包装对象
new Number(100).toFixed(); //100
//或者
(100).toFixed(); //100
通常使用包装对象的原因之一,就是需要扩展对象或者持久化保存状态,这也是因为基本类型并不是对象导致的。
当我们不使用new
来调用包装器时,包装器会通过传递给他的参数转换成一个基本类型的值,这种方式也常常用来处理类型转换。
typeof Number(1); //number
typeof Number("1"); //number
typeof String(1); //string
typeof String("1"); //string
typeof Boolean(1); //boolean
typeof Boolean(false); //boolean
错误对象
JavaScript中有一些内置的错误构造函数,比如:Error、SyntaxError、TypeError
等等,通过这些构造函数创建的错误对象具有下列属性:
- name 创建该对象的构造函数名称
- message 创建错误对象时传递给构造函数的字符串
错误对象还有一些其他属性,比如发生错误的文件行号和文件名,这个由于不同浏览器实现不同,因此并不可靠。
错误对象还有一个搭配操作符throw
,只有使用该操作时,js运行才会被中断。但是并不是一定得是错误对象才能使用throw,我们可以操作任何对象,利用这个特性我们可以创建一个自定义的错误对象。
try {
throw {
name: "自定义错误",
message: "xxxxx",
remedy: test, //指定一个处理该错误的特殊函数
}
} catch (error) {
error.remedy(); //特殊处理
}
错误类型的构造函数,即便不使用new
操作符,其表现行为和带new
操作符一致。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据