现如今JavaScript早已不仅仅是网页特效脚本了,更多是用来构建大规模的Web应用,所以语言规范的制定者们也逐渐意识到要对JS进行语法方面的规范,并且有意地引导开发者编程习惯,消除一些不规范和不安全的语法,进而更好的满足以后大规模开发的要求,ES5规范中的严格模式就是其中重要的一环,今天我们就来详细介绍一下严格模式的使用以及它对语法的种种限制和规范。
启用严格模式只需使用‘use strict‘;这条语句即可,对于支持严格模式的浏览器,在声明‘use strict‘;语句之后的代码都将会在严格模式的限制下执行,对于不支持严格模式的浏览器,这个声明只是一个简单的字符串语句,不会产生任何影响,而我们在严格模式下编写的代码是规范的,也可以很好的运行,所以无需担心兼容性问题,下面是严格模式的一个简单声明:
'use strict'; console.log("I'm running in strict mode");
需要注意的是,严格模式的声明必须放在脚本的第一行,否则整个脚本将会以正常模式运行。
严格模式应用在两种作用域,一种是整个脚本作用域,一种是单个函数作用域,下面先介绍脚本作用域:
<script type="text/javascript"> 'use strict'; console.log('strict mode comes into effect.'); </script> <script type="text/javascript"> console.log("strict mode doesn't take control of me."); </script>
上面我们使用了两个script标签,在第一个标签内部我们声明了严格模式,第二个没有,其结果将会是,严格模式只在第一个标签内部起作用,第二个标签内部的脚本将会以正常模式运行。如果上面的script标签引用了外部的JS文件,其结果将会是一致的。需要注意的是在单个脚本中使用严格模式,对文件合并时要特别小心,前一个文件的严格模式会对下一个文件产生不可预期的影响。
严格模式也可以在单个函数内声明,这种情况下,严格模式只对函数内代码起作用:
function a() { 'use strict'; console.log("I'm running in strict mode"); function b() { console.log("I'm also in strict mode"); } } console.log('strict mode takes no effect here');
在函数作用域内启用严格模式相对安全,所以考虑到我们开发中要对文件进行合并,最好把每个脚本文件放到立即执行函数内部,在这个函数内部声明严格模式就没有后顾之忧了:
;(function() { 'use strict'; console.log("I'm running in strict mode"); function b() { console.log("I'm also in strict mode"); } })();
关于为什么要在立即执行函数前面加个分号,这里我们引入一个小插曲。假如有两个文件:A.js和B.js,在发布时将他们合并在了一起:
var basePlus = function(baseNumber) { return function(number) { console.log(baseNumber + number); } } (function(name) { console.log('hello ' + name); })('Scott');
上面代码中,定义basePlus函数时没有加上分号,所以这段代码会被解释为:
var basePlus = function(baseNumber) { return function(number) { console.log(baseNumber + number); } }(function(name) { console.log('hello ' + name); })('Scott');
其结果是,下面的立即执行函数并未达到预期的效果,而是里面的函数作为参数传递进了basePlus函数,而Scott作为参数传递到了basePlus返回的函数中,打印结果如下:
所以,这里我们要在立即执行函数前面加上分号,即使像下面这样被拼接在一起也不会出问题:
var basePlus = function(baseNumber) { return function(number) { console.log(baseNumber + number); } };(function(name) { console.log('hello ' + name); })('Scott');
上面介绍了声明严格模式的两种场景,接下来我们一一介绍严格模式都做了哪些规范:
1. 变量必须使用var声明,杜绝不小心将本地变量声明成一个全局变量
在常规模式下,如果我们声明一个变量时省略了var关键字,解析引擎会自动将其声明为全局变量,但在严格模式下,会直接抛出异常,不会为我们转为全局变量:
'use strict'; myVariable = 3; //Uncaught ReferenceError: myVariable is not defined
2. 对超出权限的操作显示报错,不再做静默失败处理
常规模式下,我们可以做很多不合法的操作,比如给NaN赋值,NaN是不可写的变量,但我们尝试更新它时收不到任何的错误反馈信息,严格模式就不同了,它会显示的抛出异常,下面是一些非法的操作:
'use strict'; NaN = 3; //Uncaught TypeError: ... var person = {}; Object.defineProperty(person, 'name', { writable: false, value: 'Scott' }); person.name = 'John'; //Uncaught TypeError: ... var person2 = { get name() { return 'Scott' } }; person2.name = 'John'; //Uncaught TypeError: ... var person3 = { name: 'Scott' }; Object.preventExtensions(person3); person3.age = 20; //Uncaught TypeError: ...
在上一篇博客中,关于Object的增强我们也介绍到了很多非法操作在严格模式下的运行状态,同学们也可以去看一下。
3. 禁止删除变量和对象中不可删除的属性,显示报错
我们都知道,通过var声明的变量是不可删除的,在常规模式下,试图删除会静默失败,但在严格模式下会显式抛出异常;同样的,试图删除对象中不可删除的属性也会显式报错:
'use strict'; var myVariable = 3; delete myVariable; //Uncaught SyntaxError: ... delete Object.prototype; //Uncaught TypeError: ... var person = {}; Object.defineProperty(person, 'name', { configurable: false, value: 'Scott' }); delete person.name; //Uncaught TypeError: ...
4. 禁止对象属性重名
常规模式下,如果我们在对象中定义重复的属性,后定义的值会覆盖先定义的那个,ES5的严格模式规定,对象中不允许定义重复的属性,否则会显式报错。但博主在测试时发现,严格模式下的代码和常规模式下并无两样,原因可能在于Chrome支持一部分的ES6新功能,而ES6中存在相关的一个bug:Bug 1041128。
'use strict'; //it should throw a SyntaxError in ES5 strict mode var person = { name: 'Scott' name: 'John' }; console.log(person.name);
5. 禁止函数参数重名
常规模式下,如果在定义函数不小心声明了重复的参数名,后一个重名参数会覆盖前一个重名参数,虽然arguments里是可以访问到每个参数值的,但有时候也会遇到意想不到的结果:
var b = 0; function sum(a, a, c) { return a + b + c; } console.log(sum(1, 2, 3)); //5
上面代码中,我们不小心将参数名b写成了a,恰巧外层有个变量b,所以运算结果就变得不可预期了,结果是实参2将实参1覆盖,2 + 0 + 3 = 5。这个问题在严格模式下可以很好地避免,因为严格模式在语法层面约束了函数的定义,规定不允许有参数名重复:
'use strict'; var b = 0; function sum(a, a, c) { //Uncaught SyntaxError: ... return a + b + c; } console.log(sum(1, 2, 3));
6. 禁止使用八进制数字
以0开头的八进制数字常常会让开发者迷惑,严格模式禁止以0开头的八机制表示法,另外,ES6已经支持新的语法标准,八进制以0o来表示,这样一来就与16进制的0x形成统一的语法格式:
'use strict'; var a = 017; //Uncaught SyntaxError: ... var b = 0o17; //ES6 Octal syntax: 8 + 7 = 15
7. 禁止使用with语句
最开始使用with语句时的心情是激动的,因为with语句将指定对象作为当前的作用域,可以很直接地存取对象的属性,使我们的代码变得更简洁。但同时它又是存在问题的,因为解析器在执行里面的代码时,会去检查对象中是否存在参与运算的属性,如果有则使用,没有的话则向上查找,这个过程在一定程度上降低了代码的执行性能,并且很难优化,另外,在可读性方面也表现极差,我们来看如下代码:
var name = 'Scott'; var person = getPerson(); with(person) { name = newName; }
上面代码中,我们很难在代码运行期之前确定person对象,所以也不清楚with语句将会更改最上面定义的name变量还是person的name属性。为了规范这种行为,严格模式禁止使用with语句,如若使用,会立即抛出异常:
'use strict'; var name = 'Scott'; var person = getPerson(); with(person) { //Uncaught SyntaxError: ... name = newName; }
8. 强制为eval创建新作用域
常规模式下,使用eval函数可能会影响当前作用域或全局作用域,给程序的运行结果带来不确定性,严格模式为JavaScript程序创建了第三种作用域:eval作用域。eval函数中的字符串只能在eval作用域内运行,其结果不会影响外层作用域,下面这两种形式都可以使eval在严格模式下运行:
'use strict'; eval("var a = 1;"); console.log(a); //Uncaught ReferenceError: a is not defined //or eval("'use strict'; var b = 3;"); console.log(b); //Uncaught ReferenceError: b is not defined
可以看到,严格模式下执行eval函数不会对当前作用域产生作用。需要注意的是,如果不是直接执行eval函数,就不会进入严格模式,下面这些方式都不会以严格模式执行eval函数代码:
'use strict'; ('' || eval)("var a = 1;"); //or ('', eval) console.log(a); //1 var evl = eval; evl("var b = 3;"); console.log(b); //3 function exec(evl) { evl('var c = 5;'); console.log(c); //5 } exec(eval);
我们也看到了,虽然声明了严格模式,但几个结果都跟常规模式没什么两样,这一点我们在开发中要特别注意,不能心想声明了严格模式就可以放心地编写代码了,没准隐患就潜伏在其中了呢。
最后,大家可能会问,如果非要在严格模式下使用eval函数处理一个字符串,该如何将结果反映到当前作用域呢,我们需要像下面这样:
'use strict'; var result = eval("var sum = 1 + 3 + 5; sum;"); console.log(result); //9
9. 禁止对eval和arguments做非法操作
常规模式下的JavaScript的随意性较大,eval和arguments可以有很多稀奇古怪的用法,程序虽然可以运行,但这给代码的可读性、可维护性也带来了一些问题,下面这些代码段应尽量避免:
function eval() { console.log('define a function called eval'); }; console.log(eval); function evalX(eval) { console.log('define a function with the eval keyword as parameter name'); } console.log(evalX); function arguments() { console.log('define a function called arguments'); }; console.log(arguments); var func = new Function('arguments', 'return 3;'); console.log(func); var eval = 1; console.log(++eval); //2 var arguments = 3; console.log(++arguments); //4 var person = { set name(arguments) { console.log(arguments); //Scott } }; person.name = 'Scott'; try { console.log(unknownVariable); } catch(arguments) { console.log(arguments); //ReferenceError: unknownVariable is not defined }
在严格模式下,解释器将在语法层面禁止eval和arguments做上面这些操作,一旦声明了严格模式,这些操作将会直接抛出语法异常。需要注意的是,eval只是不能在定义函数时作为一个函数参数名,它是可以在调用一个函数时作为实参传递进去的,这一点在第8条中有体现,另外,对于使用new Function创建函数的这种形式,我们在外层声明严格模式是无效的,必须在函数体内部明确声明严格模式,代码如下:
'use strict'; //strict mode is beyond the control var func = new Function('arguments', "return 3;"); console.log(func); //declaring the 'use strict' in function body is a must var func = new Function('arguments', "'use strict'; return 3;"); console.log(func);
10. arguments不再追踪参数变化
常规模式下,在执行函数时如果我们更改参数的值,操作结果会立即反映到arguments对象中,反之,更改arguments对象中的值,结果也会立即反映到参数上:
var fn = function(a, b, c) { a = 10; console.log(arguments[0]); // 10 arguments[1] = 20; console.log(b); // 20 }; fn(1, 2, 3);
严格模式约束了这种行为,将arguments对象与参数分离,更改只会影响自己,不会对彼此都产生影响,严格模式下,上面的操作结果如下:
'use strict'; var fn = function(a, b, c) { a = 10; console.log(arguments[0]); // 1 arguments[1] = 20; console.log(b); // 2 }; fn(1, 2, 3);
11. 禁止使用arguments.callee
callee作为arguments对象的一个属性,我们可以在函数内部调用它来获取当前正在执行的函数,这在某些场景下特别有用,尤其是在匿名的递归函数中。假如我们有一个数组,现在需要对数组内的每个元素求阶乘,可能像下面代码这样:
var factorialArray = [1, 2, 3, 4, 5].map(function(n) { return (n < 2) ? 1 : arguments.callee(n - 1) * n; }); console.log(factorialArray); // [1, 2, 6, 24, 120]
我们使用数组新增函数map来对数组进行处理,对每个数组元素都调用匿名函数对其求阶乘,在这里我们使用了arguments.callee获取当前执行的匿名函数,然后递归地调用,最终求得每个元素的阶乘值。
那么为什么严格模式要禁止arguments.callee呢,其中一个原因是不能进行内联和尾递归的优化。下面这段代码我们使用了一个for循环来求循环中每个数值的阶乘:
function getFactorial(n) { return (n < 2) ? 1 : arguments.callee(n - 1) * n; } function calculate() { for (var i = 0; i < 100; i++) { console.log(getFactorial(i)); } }
由于每次调用getFactorial函数,在它的内部都会查找当前正在调用的函数,所以原本解析器可以对getFactorial函数做内联处理来提高性能的,现在使用了callee,大大影响了解析器的优化策略。下面这段代码是一个尾递归的例子:
function factorial(n, result) { if (n < 2) return result; return factorial(n - 1, n * result); } console.log(factorial(5, 1));
我们看到,这个递归和常规的递归不同,它每次调用自身时都附带有上次调用后的结果,解析器因此会对这段代码进行优化,优化后的代码运行时不再占用太多的栈,每次只保留内层函数的调用记录,大大节省了内存,并且不会出现内存溢出的情况。
但是如果我们在上面的递归中使用了arguments.callee,情况就不同了:
function factorial(n, result) { if (n < 2) return result; return arguments.callee(n - 1, n * result); } console.log(factorial(5, 1));
这段代码虽然结果是一样的,但解析器无法优化它,因为arguments保存了函数调用栈的信息,解析器优化时便不能随意修改栈信息,因此会放弃对这段代码的优化。
以上就是内联和尾递归的优化问题,其实还有一个很重要的原因,那就是使用arguments.callee会更改函数中this的指向:
function factorial(n, result) { console.log(this); if (n < 2) return result; return arguments.callee(n - 1, n * result); } console.log(factorial(5, 1));
我们在代码中打印函数当前执行的上下文对象,结果如下:
可以看到,除了第一次外部调用外,使用arguments.callee的调用中,this都是arguments对象,如果函数中引用到了当前上下文,这也会对程序的结果造成不确定性。
所以,严格模式中禁止使用arguments.callee调用,如果声明了严格模式,遇到arguments.callee会抛出异常。
最后,如果不能使用arguments.callee,又如何在匿名函数中调用自身呢?从ES3开始命名函数表达式被引入语言特性中,所以我们可以像下面代码一样使用:
var factorialArray = [1, 2, 3, 4, 5].map(function factorial(n) { return (n < 2) ? 1 : factorial(n - 1) * n; }); console.log(factorialArray); // [1, 2, 6, 24, 120]
使用命名函数表达式同样可以在函数内部调用自身,在现代浏览器中,并不会在外部作用域中创建一个变量,另外性能比使用arguments要好很多,所以推荐使用。
12. 禁止this指向全局
常规模式下,JavaScript太过于灵活,如果对语言特性了解的不够深入,常常会因为失误的的调用,造成不一致的结果,下面这段程序演示了this的指向问题:
var name = 'Global'; function Person() { this.name = 'Scott'; } Person(); console.log(name); //Scott var person = { name: 'John', getName: function() { return this.name; } }; var getName = person.getName; console.log(getName()); //Scott
我们首先定义了Person构造函数,里面设置它的成员变量,但随后并没有使用new创建一个实例,而是直接调用了这个函数,其结果是函数中的this执行时指向了全局,于是就把全局的name变量更改为了Scott;后面的person对象里我们定义了name属性和getName函数,但后面我们是间接调用这个函数的,函数中的this也指向了全局,其结果没有返回person的name属性值,而是全局最新的name变量值Scott。可见常规模式下允许this指向全局是一件很具隐患的操作。
在严格模式中,this不被允许指向全局,如果运行时试图指向全局,this将会变为undefined,上面代码在严格模式中调用Person函数将会抛出异常,因为不能为undefined设置属性。我们可以用下面代码测试:
'use strict'; var func = function() { return this; }; console.log(func() === undefined); //true
另外,常规模式中,this总是一个对象,即使运行时上下文是基础类型,也会被自动装箱为对象:
var func = function() { return this; }; console.log(func.call('hello')); // Boxed String console.log(func.call(1)); // Boxed Number console.log(func.call(true)); // Boxed Boolean console.log(func.apply(null)); // Window console.log(func.apply(undefined)); // Window
而在严格模式中,不再进行自动装箱机制,从性能上也得到了很大的提升:
'use strict'; var func = function() { return this; }; console.log(func.call('hello') === 'hello'); // true console.log(func.call(1) === 1); // true console.log(func.bind(true)() === true); // true console.log(func.apply(null) === null); // true console.log(func.call(undefined) === undefined); // true
我们可以很容易看出,他们前后的类型是完全一致的。
13. 禁止使用function直接引用caller和arguments
上面我们刚刚介绍过,严格模式下禁止arguments.callee的用法,同样地,出于对性能和安全方面的考量,严格模式禁止通过函数名直接访问函数的调用栈相关信息,因此下面这段代码会直接抛出异常:
function testFunc() { 'use strict'; console.log(testFunc.caller); //Uncaught TypeError: ... console.log(testFunc.arguments); //Uncaught TypeError: ... } function main() { testFunc(); } main();
14. 函数必须声明在整个脚本或函数层面
常规模式下,在语句块中声明函数会使得程序的结果不可预料,也会使可读性变得很糟糕,下面这段代码在不同浏览器中会有不同的结果:
var condition = true; if (condition) { function doSomething() { console.log('1'); } } else { function doSomething() { console.log('2'); } } doSomething();
在Mac平台上,Chrome和Firefox会根据condition的值选用对应的函数,而Safari不会理睬condition,直接选用后面定义的函数,所以结果永远打印出2。这个场景是ECMAScript中的灰色地带,应尽量避免。
严格模式禁止在语句块中声明函数,下面代码在Chrome测试时,不会抛出语法异常,但会忽略doSomething函数的声明,其结果是,调用doSomething时抛出异常,提示函数未定义:
'use strict'; if (true) { function doSomething() { console.log('1'); } } else { function doSomething() { console.log('2'); } } for (var i = 0; i < 5; i++) { function doSomething() { console.log('3'); } } doSomething(); //Uncaught ReferenceError: doSomething is not defined
所以不应该在块语句中进行函数声明,而是要放在脚本和函数层面上声明。
15. 新增一些保留字
ES5本质上来讲只是一个语言优化的过渡,约束了晦涩和不安全的语法,为以后的高级语法铺平道路。所以在ES5中新增了一些保留字,严格模式下,不能使用他们作为变量名或参数名:
implements, interface, let, package, private, protected, public, static, yield.
以上就是ES5严格模式的全部内容,如有遗漏或不一致的情况,再加以补充。