写了这么多年JS其实也没认真学习过它的规范,所以现在恶补一下。以前一直在写的JS基本上是一个不怎么严格的ES3规范,虽然它有很多古怪的行为,但是用着用着就习惯了。后来ES经历了什么变化,就没太系统化地学习过了,只是看到人家怎么写自己就学着怎么写,所以这里会发现一些其实自己已经一直在使用功能。ES5是完全兼容ES3的,主要变化是增加了一些标准API扩展和(提倡使用的)严格模式。(顺带一提,ES4的正式版发布失败了,因为这个版本步子跨得太大被废弃了,曾经在Adobe Flex中使用过ES4。)
标准API扩展
Date支持ISO8601格式
这个功能已经在工作上用了很久了,例如在利用Jackson将JAX-RS(Jersery实现)的内容对象序列化为JSON时,日期时间类内容默认就是以ISO8601格式呈现。以前还很喜欢用毫秒纪时的方式序列化,但对于人类可读性太差了,这无形中会阻碍调试问题的进度。
var iso8601 = new Date().toISOString(); // 可直接格式化为ISO8601
alert('ISO8601 format: ' + iso8601);
alert('Can new Date(ISO8601): ' + (new Date(iso8601).toISOString() == iso8601)); // 还可以拿这个格式创建Date实例
var now = Date.now(); // 直接获取毫秒纪时
alert('Can new Date(now): ' + (new Date(now).getTime() === now));
String添加trim()方法
这个功能也是老朋友了,它的行为与Java的String::trim()
方法一致。
alert('|' + ' \rabc\n'.trim() + '|');
// 在ES3时代都写这种以正则表达式匹配来代替trim()方法
alert('|' + ' \rabc\n'.match(/\s*([^\s].*[^\s])\s*/)[1] + '|');
字符串字面量可以使用下标操作符
这个特性貌似没什么实用性……
alert('abc'[1]);
内建JSON库
早年写AJAX应用时还没有内建JSON库,要么引用外部的JSON库、要么就直接用eval()
,这导致那个时候的AJAX应用一堆XSS/CSRF漏洞。后来变成内建库之后瞬间就普及了。
// 序列化为JSON字符串时还可以自定义值替换器和格式化缩进的大小
var json = JSON.stringify({ x: 1 }, null, 4);
alert('JSON with indent: ' + json);
alert('JSON parse OK: ' + (JSON.parse(json).x === 1));
// 反序列化时除了RFC 4627指定的对象和数组格式,还支持ES内建字面量
alert('JSON parse OK: ' + JSON.parse('3'));
alert('Date.prototype.toJSON: ' + new Date().toJSON()); // 所有内建的数据类型的原型都添加了toJSON()方法
函数bind()方法切换“this”对象
这个Function.prototype.bind()
方法就是Prototype.js库(这个库已作古——jQuery都快没新人在用了)的bind()
方法标准版。
var obj = {
x: 1,
getX: function () {
return this.x;
}
};
var a = obj.getX(); // getX()方法作用在obj上
var getX = obj.getX;
// 这样调用getX()方法便相当于this为运行环境的全局对象
alert('window as "this", x will be: ' + getX()); // 在浏览器里当然便是window对象了,但window里并没有x这个全局变量
getX = getX.bind({ x: 2 }); // 用bind()方法将函数的this切换到那个临时对象上
alert('Temp obj as "this", x will be: ' + getX());
函数的apply()方法可以接受类数组参数
function f(x, y) {
alert('x: ' + x + ', y: ' + y);
}
f.apply(null, { 0: 'x', 1: 'y', length: 2 });
数组增加一系列常用的内建方法
早年接触JS的时候真心觉得它的数组的API设计很奇葩,举例想想怎么在JS的数组中实现类似Java的List::remove(int i)
和List::add(int i, E e)
方法?每次想起这个我都得Google一下才记得它们分别是[...].splice(i, 1)
和[...].splice(i, 0, e)
。当后来发现连字符串都有的indexOf()
方法数组竟然没有,在jQuery普及之前基本都靠写for
循环来实现,想想也真是搞笑……当时的我是用一个借口说服了自己——ES的规范并不像Java那样有一个依赖于Object::equals(Object o)
的逻辑性的对象间相等的逻辑,它就算内建了indexOf()
方法也只能够用===
来判断对象间是否严格相等,有时候这样又会显得很不实用(JS很多荒诞的问题都是由于这个相等判断而来的)。不过后来总算内建了indexOf()
和lastIndexOf()
方法了,同时间还带来了一些常用函数式编程方法。
var array = [];
for (var i = 0; i
Object.seal()与Object.freeze()
JS的鸭子类型(Duck Typing)十分方便写一些短小的的代码,但由于对象的属性是可以随便存储/删除的,这对于各种功能性的代码库的封装有极大的坏处,很有可能随便一两个组件的属性被修改后便不能够正常运作。对于JS库的作者来说,Object.seal()
和Object.freeze()
方法就很适合用于更完整的封装。
seal()
封锁方法用于将对象的属性列表固定下来,而已固定下来的属性还是可以读取和修改的,但就不可以给这个对象增加新的属性和删除已有属性。
freeze()
冻结方法与seal()
有类似的作用,不过属性也变成只读的了、连修改也被禁止了。
var obj1 = { x: 1, y: 2 };
Object.seal(obj1);
var obj2 = JSON.parse(JSON.stringify(obj1)); // 一个低效但方便的深度拷贝方案
Object.freeze(obj2);
function testSealed(obj) {
alert('isSealed: ' + Object.isSealed(obj)); // 被冻结了的对象也会被视为已被封锁
obj.z = 3; // 不会生效
delete obj.y; // 不会生效
obj.x = 10; // 冻结后不会生效
alert(JSON.stringify(obj));
}
testSealed(obj1);
function testFrozen(obj) {
alert('isFrozen: ' + Object.isFrozen(obj));
testSealed(obj);
}
testFrozen(obj2);
Object.keys()获取对象可枚举属性名列表
以前如果我们想获得一个对象所有的属性名,那就必须写一个for
循环,有了这个方法之后就方便一点了。
var obj = { a: 1, b: 2, c: 3 };
var keys = [];
for (var k in obj)
keys.push(k);
alert('Keys are the same: ' + (JSON.stringify(keys) == JSON.stringify(Object.keys(obj))));
另外一提,可以在for .. in
代码块中枚举出来的属性名都是可枚举属性名。
Object.getOwnPropertyNames()获取可枚举与不可枚举属性名列表
参照上一节的内容,就可以发现Object.getOwnPropertyNames()
的返回值总是包含Object.keys()
的结果。但什么是不可枚举属性?以数组为例,它的length
属性就是不可枚举属性,因为它不可以在for .. in
代码块中枚举出来。
var ary = [1, 2, 3];
alert(JSON.stringify(Object.getOwnPropertyNames(ary)));
另外,
用Object.create()继承/扩展原型
ES3的时候引入了以构造函数的.prototype
属性来定义类的实例属性/方法的功能,但没有涉及到不同类之间如何继承/扩展的功能。Object.create()
方法就是为了弥补这个缺憾。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
};
function Point() {
Shape.call(this); // 子类调用父类的构造函数
}
Point.prototype = Object.create(Shape.prototype); // 子类继承父类的所有方法
Point.prototype.constructor = Point; // 重新设置原型的构造函数
var point = new Point();
point.move(10, 10);
alert(JSON.stringify(point));
alert('point instanceof Shape: ' + (point instanceof Shape));
alert('point instanceof Point: ' + (point instanceof Point));
使用Object.create()
方法只能够实现单继承。如果要实现多继承/混入(MixIn),则需要用到后续的新规范的API才可以,后面会有文章继续探讨。
Object.create()
还可在继承/扩展的时候定义一些额外属性,还可以对属性做一些精细的读写配置,例如可以创建上一节所提及的、不可枚举的属性。
var obj = Object.create({ a: 1 }, {
b: { value: 2, enumerable: false }
});
alert('Has property b: ' + (obj.b === 2));
alert('Property b is not enumerable: ' + (Object.getOwnPropertyNames(obj).indexOf('b') !== -1));
alert('Property b not in keys: ' + (Object.keys(obj).indexOf('b') === -1));
定义新属性时的配置项也挺丰富的,详情见下一节。
使用Object.defineProperty()与Object.defineProperties()定义属性
使用这类型方法定义的属性默认都是不可删除、不可枚举且不可赋值的,就像这些属性被调用过一次Object.freeze()
一样。
var obj = {};
var a = 1;
Object.defineProperty(obj, 'a', {
configurable: true, // 是否可删除,默认为false
enumerable: true, // 是否可枚举,默认为false
// writable: true, // 是否可赋值,默认为false
// value: function () {
// return a;
// }, // 初始值,可以是一个值或函数,默认为undefined
set: function (v) {
alert('setting a to ' + v);
a = value;
}, // setter拦截函数,默认为undefined,设置了setter就相当于writable肯定为true,所以便不可再配置writable
get: function () {
alert('getting a as ' + a);
return a;
} // getter拦截函数,默认为undefined,设置了getter便不再可以配置前面的value
});
var b = obj.a;
obj.a = 2;
alert('Has property a: ' + (Object.keys(obj).indexOf('a') !== -1));
delete obj.a;
alert('Property a deleted: ' + (Object.keys(obj).indexOf('a') === -1));
Object.defineProperties(obj, { // 相当于复数版本的Object.defineProperty()
b: { value: b },
c: {}
});
alert('Property b not in keys: ' + (Object.keys(obj).indexOf('b') === -1));
alert('Property c not in keys: ' + (Object.keys(obj).indexOf('c') === -1));
alert('Property b is OK: ' + (b === obj.b));
alert('Property c: ' + obj.c);
Object.getOwnPropertyDescriptor()获取属性的描述
var obj = {};
Object.defineProperty(obj, 'x', { value: 'x' });
alert(JSON.stringify(Object.getOwnPropertyDescriptor(obj, 'x'), null, 4));
Object.getPrototypeOf()获取对象的原型
一时间想不出这个方法哪里比instanceof
操作符优秀……两个原型之间只可以用===
来对比,而instanceof
则可以适用于继承。
var proto = {};
var obj = Object.create(proto);
alert('Prototype is the same: ' + (Object.getPrototypeOf(obj) === proto));
Object.preventExtensions()阻止扩展对象
顾名思义,就是阻止对象可以使用Object.defineProperty()
/Object.defineProperties()
添加新属性,甚至连Object.create()
也会被禁止。即使直接操作对象的原型.__proto__
来添加新属性也是会被阻止。.
操作符添加新属性也会被阻止,但不会报错,而在严格模式(下文详述)中,这样操作还会报错。
var obj1 = {};
var obj2 = Object.preventExtensions(obj1);
alert('Prevented object is the original one: ' + (obj1 === obj2));
try {
Object.defineProperty(obj2, 'x', { value: 1 });
} catch (e) {
alert(e);
}
try {
Object.create(obj1, { x: { value: 1 } });
} catch (e) {
alert(e);
}
try {
obj2.__proto__ = { y: 2 };
} catch (e) {
alert(e);
}
(function () {
'use strict';
try {
obj2.z = 3;
} catch (e) {
alert(e);
}
})();
obj2.z = 3;
alert('Can not new property by "." operater: ' + (obj2.z !== 3));
Object.isExtensible()判断对象是否可扩展
var obj = {};
Object.preventExtensions(obj);
alert('Is extensible: ' + (Object.isExtensible(obj)));
新语法
对象字面量的函数成员简化语法
重复而且罗嗦的: function
被省略了。
var obj1 = {
f: function (x) {
alert('Old obj function syntax: ' + x);
}
};
obj1.f('x');
var obj2 = {
f(x) {
alert('New obj function syntax: ' + x);
}
};
obj2.f('x');
对象字面量可声明setter/getter
有了前面那个新语法,就可以取代ES3时代就存在、但非标准的Object.prototype.__defineSetter__()
/Object.prototype.__defineGetter__()
,使得语法更统一了。
var obj = {
set x(v) {
alert('Setting obj.x to ' + v);
this._x = v;
},
get x() {
alert('Getting obj.x as ' + this._x);
return this._x;
}
};
obj.x = 1;
var x = obj.x;
对象字面量可使用保留字作为属性名
个人觉得不是很建议用,因为很容易使得代码更难阅读了。
var obj = { new: 1, function: 1 };
alert('Can use reserved word as properties: ' + (obj.new === obj.function));
对象和数组字面量最末尾成员可带逗号
其实这个特性ES3时代就一惊有,但是不是标准范围内的,而且有某些浏览器的JS引擎并不支持这种语法。这种语法方便了用模板生成JS/JSON文件的程序,这样就不用担心多一个逗号代码就不可以执行了。
var obj = {
x: 'x',
};
var ary = [
'x',
];
alert('Legal trailing commas: ' + (obj.x === ary[0]));
多行字符串
这语法对于前端编程十分重要,因为总是少不免要在JS代码内嵌HTML。只要在字符串的每一行结束位置添加一个反斜杠\
就可以用多行的方式表达符串了,\
本身会被忽略。注意,这个“多行”是在源代码层面的,字符串的值本身是不会带换行符的,而且反斜杠后必须是换行符,否则会出现语法错误。
alert('\
this \
is \
a \
multi-line \
string');
严格模式
ES5最重要的是引入了严格模式,在一个代码作用域的最开头一行写上'use strict'
,就会在该作用域开启严格模式,在作用域的中间写这一句开启不了严格模式的:
(function () {
'use strict';
// 这里只能够写严格代码
// ......
})();
// 外面还是可以按照ES3的方式写代码
// ......
显然,如果在JS文件的最开头处声明了严格模式,那么这份源代码的所有作用域都是严格模式;而只在某个局部作用域开启严格模式的话,其它位置默认还是会以ES3的方式执行代码。
严格模式在语法上只是ES3的子集,它主要目的是禁止了历史上JS一些臭名昭著的烂代码。那下面看看各种限制都有哪些。
定义变量前必须声明变量
ES3的一个“特性”就是变量随便就可以定义。其实也有一些语言是这样设计的,例如Python就是这样(因为它根本没有声明变量的语法)。但这样随便定义变量,很容易就会与嵌套作用域的代码的变量重名,而且没有声明就定义的变量在ES3上属于全局变量,这很容易便会落入过度使用全局变量的这种编程最糟实践(但Python不会有这个问题)。
(function () {
'use strict';
try {
x = 'x';
alert(x);
} catch (e) {
alert(e);
}
var z = 'z';
alert(z);
})();
y = 'y';
alert(y);
eval()执行的代码属于独立作用域
eval()
的确是一个很方便但又很容易被滥用的功能。每个eval()
内的代码块都成为独立的作用域后,就可以防止不小心地篡改执行环境的上下文。
(function () {
'use strict';
try {
eval('var x = "x";');
alert(x);
} catch (e) {
alert(e);
}
var z = eval('var z = "z"; z');
alert(z);
})();
eval('var y = "y";');
alert(y);
取消八进制字面量
这个举手同意,因为八进制字面量只由0
至7
组成,一眼看过去其实很难将它与十进制字面量区分(相反地十六进制字面量就没这个问题)。字符串的八进制转义也会被禁止。
(function () {
'use strict';
alert(010);
alert('\010');
})();
alert(010);
alert('\010');
顺带一提,不论是否在严格模式下,parseInt()
也不再支持八进制,所以有parseInt('010') => 10
。
禁止delete
禁止删除变量:
'use strict';
var x = 1;
delete x;
禁止删除参数:
'use strict';
function f(x) {
delete x;
}
f('x');
禁止删除函数:
'use strict';
function f(x) {}
delete f;
使用Object.defineProperty()
/Object.defineProperties()
/Object.create()
定义的属性如果设置configurable: false
,删除这个属性时在严格模式下就会报错。非严格模式下虽然能执行删除语句且不会报错,但不会有任何效果。
var obj = {};
Object.defineProperty(obj, 'x', { value: 'x', configurable: false });
(function () {
'use strict';
try {
delete obj.x;
} catch (e) {
alert(e);
}
})();
delete obj.x;
alert('obj.x is still here: ' + (obj.x === 'x'));
禁止with操作
ES3时代with
操作也已经是不建议使用的了,因为很容易混淆变量作用域。现在干脆就在严格模式废掉这个操作。
'use strict';
var obj = { x: 'x' };
with (obj) {
alert('Illegally get x: ' + x);
}
禁止函数声明重复名字的参数
在ES3中,重复声明的参数名的值以最后的那个为准。其实常规的逻辑代码没必要使用重复变量名,因为它会带来各种阅读/解读上的麻烦。严格模式就直接禁止这种用法了,支持。
'use strict';
function f(x, x) {
alert(x);
}
f(1, 2);
禁止对象声明重复名字的属性
与上面类似的原因,严格模式就连重复属性名也禁止了。但实际上目前的浏览器(例如我常用的Chrome和Firefox)并没有实现这个限制,所以下面的代码并没有如ES5的要求报错。
'use strict';
var obj = { x: 1, x: 2 };
alert(obj.x);
不再可以访问函数的调用信息
一时半刻想不通为什么要禁止这个用法。禁用它们唯一比较合理的原因是因为有并发问题?还没试过不能确定。
值得注意的是,函数的声明位置会影响受限制的范围,如果是非严格模式外定义的函数在严格模式中调用,f.caller
会变成null
、f.arguments
、f.arguments.callee
则依然可以访问。
function f() {
alert(f.caller + '\n' + f.arguments.callee + '\n' + f.arguments);
}
(function () {
'use strict';
function f1() {
alert(f1.caller + '\n' + f1.arguments.callee + '\n' + f1.arguments);
}
try {
f1();
} catch (e) {
alert(e);
}
f();
})();
function f2() {
alert(f2.caller + '\n' + f2.arguments.callee + '\n' + f2.arguments);
}
f2();
使用arguments的新限制
另外,虽然f.arguments
在严格模式下不能访问,但特殊变量arguments
依然允许在严格模式下使用,但对它重新赋值将会报错。这个限制与前面一样,函数必须在严格模式下声明才会受到保护。同样地,严格模式还禁止声明名叫arguments
的变量、参数和函数,因为这也相当于对它进行了重新赋值。
'use strict';
function f() {
arguments = [];
}
function arguments() {}
var arguments = 0;
function f1(arguments) {}
而且,严格模式下不再同步arguments
的元素的值。
(function (x) {
x = 1;
alert('x: ' + x + ', arguments[0]: ' + arguments[0]);
})(0);
(function (x) {
'use strict';
x = 1;
alert('x: ' + x + ', arguments[0]: ' + arguments[0]);
})(0);
禁止在if、for、while、switch代码块内部声明函数
但貌似现在的浏览器的严格模式并没有禁止这个用法。
'use strict';
for (var i = 0; i
不过当然了,不禁止也不提倡这样写代码。
禁止使用新添加的关键字作为变量名、函数名
ES5当然引入了一些新的关键字,但在非严格模式下这些关键字不会激活,所以是可以使用它们作为变量名、函数名的。但在严格模式便不允许使用这些关键字了。这个限制貌似与上文「对象字面量可使用保留字作为属性名」冲突,但实际上是不会的,因为那个是指属性名。
'use strict';
var public = 1;
不过有三个特殊的关键字,它们本质上都是特殊值、是全局对象window
的只读属性的值,在非严格模式下,对它们赋值不会报错也不会生效。不过,在严格模式下,对它们赋值会报错。
(function () {
'use strict';
try { NaN = 1; }
catch (e) { alert(e); }
try { Infinity = 1; }
catch (e) { alert(e); }
try { undefined = 1; }
catch (e) { alert(e); }
})();
NaN = 1;
alert('isNaN: ' + isNaN(NaN));
Infinity = 1;
alert('isFinite: ' + isFinite(Infinity));
undefined = 1;
alert('undefined: ' + (typeof undefined == 'undefined'));
另外还要注意,任何情况下都不可以写null = 1
,因为null
真的不是变量名,对它赋值只会产生语法错误。
禁止“this”指向全局对象
在「函数bind()方法切换“this”对象」一节中也介绍过,一个不属于对象、孤悬的函数的this
会指向window
全局对象。而在严格模式则会禁止这种行为,这将会减少代码的变量错误地变为全局变量的机会。
(function () {
'use strict';
function f() {
alert('"this" is: ' + this);
}
f();
})();
function f() {
alert('"this" is: ' + this);
}
f();
所以在严格模式下,如果一个函数是构造函数,调用它的时候忘记了使用new
操作符,那么就会因为在undefined
上使用.
操作而报错;而非严格模式则会因为没有new
操作符错误地创建了一些全局变量,但不会报错。