严格模式
设计目的
早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从 ES5 进入标准的,主要目的有以下几个。
- 严格模式通过抛出错误来消除了一些原有静默错误。明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
- 严格模式修复了一些导致 JavaScript 引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。提高编译器效率,增加运行速度。
- 严格模式禁用了在 ECMAScript 的未来版本中可能会定义的一些语法。为未来新版本的 JavaScript 语法做好铺垫。
总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。
调用严格模式
严格模式可以应用到整个脚本或个别函数中。不要在封闭大括弧 {} 内这样做,在这样的上下文中这么做是没有效果的。在 eval 、Function、事件处理器属性、setTimeout() 方法中传入的脚本字符串,其行为类似于开启了严格模式的一个单独脚本,它们会如预期一样工作。
进入严格模式的标志,是一行字符串use strict。老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。
为脚本开启严格模式
use strict放在脚本文件的第一行,整个脚本都将以严格模式运行。如果这行语句不在第一行就无效,整个脚本会以正常模式运行。(严格地说,只要前面不是产生实际运行结果的语句,use strict可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面。)
<script>
'use strict';
console.log('这是严格模式');
</script>
<script>
console.log('这是正常模式');
</script>上面代码中,一个网页文件依次有两段 JavaScript 代码。前一个<script>标签是严格模式,后一个不是。
如果use strict写成下面这样,则不起作用,严格模式必须从代码一开始就生效。
<script>
console.log('这是正常模式')
;('use strict')
</script>为函数开启严格模式
use strict放在函数体的第一行,则整个函数以严格模式运行。
function strict() {
'use strict'
return '这是严格模式'
}
function strict2() {
'use strict'
function f() {
return '这也是严格模式'
}
return f()
}
function notStrict() {
return '这是正常模式'
}有时,需要把不同的脚本合并在一个文件里面。如果一个脚本是严格模式,另一个脚本不是,它们的合并就可能出错。严格模式的脚本在前,则合并后的脚本都是严格模式;如果正常模式的脚本在前,则合并后的脚本都是正常模式。这两种情况下,合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中。
;(function () {
'use strict'
// some code here
})()ES6 模块默认在严格模式下执行
严格模式中的变化
将过失错误转成异常(显式报错)
在严格模式下,某些先前被接受的过失错误将会被认为是异常。JavaScript 被设计为能使新人开发者更易于上手,所以有时候会给本来错误操作赋予新的不报错误的语义(non-error semantics)。有时候这可以解决当前的问题,但有时候却会给以后留下更大的问题。严格模式则把这些失误当成错误,以便可以发现并立即将其改正。
给未声明的变量赋值
严格模式下无法再意外创建全局变量。在非严格模式下,在赋值中错误拼写的变量会在全局对象上创建一个新属性并继续“工作”。在严格模式下,意外创建全局变量的赋值会抛出错误,变量都必须先声明,然后再使用。全局变量必须显式声明。
'use strict'
let mistypeVariable
// 假设全局变量 mistypeVarible 不存在,由于“mistypeVariable”拼写错误(缺少“a”),这行会抛出 ReferenceError
mistypeVarible = 17
for (i = 0; i < 2; i++) {
// 报错,i 未声明
// ...
}
function f() {
x = 123
}
f() // 报错,未声明就创建一个全局变量给对象属性赋值会失败
严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常。例如,NaN 是一个不可写的全局变量。在正常模式下,给 NaN 赋值不会产生任何作用; 开发者也不会受到任何错误反馈。但在严格模式下,给 NaN 赋值会抛出一个异常。任何在正常模式下引起静默失败的赋值操作(给不可写属性赋值,给只读属性(getter-only)赋值,给不可扩展对象的新属性赋值)都会抛出异常:
'use strict'
// 给不可写全局变量赋值
undefined = 5 // TypeError
Infinity = 5 // TypeError
'abc'.length = 5 // TypeError
// 给不可写属性赋值
const obj1 = {}
Object.defineProperty(obj1, 'x', { value: 42, writable: false })
obj1.x = 9 // TypeError
// 给只读属性赋值
const obj2 = {
get x() {
return 17
}
}
obj2.x = 5 // TypeError
Object.defineProperty({}, 'a', {
value: 37,
writable: false
})
obj.a = 123 // TypeError: Cannot assign to read only property 'a' of object #<Object>
// 给不可扩展对象的新属性赋值
const fixed = {}
Object.preventExtensions(fixed)
fixed.newProp = 'ohai' // TypeError非严格模式下对设置为 configurable: false 属性调用 delete 没有效果,在严格模式下,试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果),只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除。
'use strict'
delete Object.prototype // 抛出 TypeError 错误
// 删除不可配置的属性会报错
var obj3 = Object.defineProperty({}, 'p', {
value: 1,
configurable: false
})
delete obj3.p // TypeError: Cannot delete property 'p' of #<Object>严格模式要求一个对象内的所有属性名在对象内必须唯一。正常模式下重名属性是允许的,最后一个重名的属性决定其属性值。因为只有最后一个属性起作用,当代码要去改变属性值而不是修改最后一个重名属性的时候,复制这个对象就产生一连串的 bug。在严格模式下,重名属性被认为是语法错误:
'use strict'
var o = { p: 1, p: 2 } // !!! 语法错误严格模式要求函数的参数名唯一。在正常模式下,最后一个重名参数名会掩盖之前的重名参数。之前的参数仍然可以通过 arguments[i] 来访问,还不是完全无法访问。然而,这种隐藏毫无意义而且可能是意料之外的(比如它可能本来是打错了),所以在严格模式下重名参数被认为是语法错误:
function sum(a, a, c) {
// !!! 语法错误
'use strict'
return a + a + c // 代码运行到这里会出错
}严格模式禁止八进制数字语法。ECMAScript 并不包含八进制语法,但所有的浏览器都支持这种以零(0)开头的八进制语法:0644 === 420 还有 "\045" === "%"。在 ECMAScript 6 中支持为一个数字加 0o 的前缀来表示八进制数。
var a = 0o10 // ES6: 八进制严格模式禁止设置原始值的属性。不采用严格模式,设置属性将会简单忽略(no-op),采用严格模式,将抛出 TypeError 错误
'use strict'
false.true = '' // TypeError
;(14).sailing = 'home' // TypeError
'with'.you = 'far away' // TypeError在严格模式下,Reflect.set 处理程序中返回 false 会抛出 TypeError。
'use strict'
const myTarget = {}
Reflect.defineProperty(myTarget, 'foo', { value: 'bar' })
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log('set()')
return Reflect.set(...arguments)
}
})
proxy.foo = 'bar' // 'set' on proxy: trap returned falsish for property 'foo'简化变量的使用
第一,严格模式禁用 with。
第二,严格模式下 eval 不再为周围的作用域引入新变量。在正常模式下,代码 eval("var x;") 会给上层函数(surrounding function)或者全局引入一个新的变量 x。这意味着,一般情况下,在一个包含 eval 调用的函数内所有没有引用到参数或者局部变量的名称都必须在运行时才能被映射到特定的定义(因为 eval 可能引入的新变量会覆盖它的外层变量)。在严格模式下 eval 仅仅为被运行的代码创建变量,所以 eval 不会使得名称映射到外部变量或者其他局部变量:
var x = 17
var evalX = eval("'use strict'; var x = 42; x")
console.log(x === 17)
console.log(evalX === 42)相应地,如果函数 eval 被在严格模式下的 eval(...)以表达式的形式调用时,其代码会被当做严格模式下的代码执行。当然也可以在代码中显式开启严格模式,但这样做并不是必须的。
function strict1(str) {
'use strict'
return eval(str) // str 中的代码在严格模式下运行
}
function strict2(f, str) {
'use strict'
return f(str) // 没有直接调用 eval(...): 当且仅当 str 中的代码开启了严格模式时
// 才会在严格模式下运行
}
function nonstrict(str) {
return eval(str) // 当且仅当 str 中的代码开启了"use strict",str 中的代码才会在严格模式下运行
}
strict1("'Strict mode code!'")
strict1("'use strict'; 'Strict mode code!'")
strict2(eval, "'Non-strict code.'")
strict2(eval, "'use strict'; 'Strict mode code!'")
nonstrict("'Non-strict code.'")
nonstrict("'use strict'; 'Strict mode code!'")因此,在 eval 执行的严格模式代码下,变量的行为与严格模式下非 eval 执行的代码中的变量相同。
第三,严格模式禁止删除声明变量。如果使用delete命令删除一个变量,会报错。
'use strict'
var x
delete x // 语法错误
eval('var y; delete y;') // !!! 语法错误让 eval 和 arguments 变的简单
严格模式让 arguments 和 eval 少了一些奇怪的行为。两者在通常的代码中都包含了很多奇怪的行为:eval 会添加删除绑定,改变绑定好的值,还会通过用它索引过的属性给形参取别名的方式修改形参。虽然在未来的 ECMAScript 版本解决这个问题之前,是不会有补丁来完全修复这个问题,但严格模式下将 eval 和 arguments 作为关键字对于此问题的解决是很有帮助的。
第一,名称 eval 和 arguments 不能通过程序语法被绑定或赋值。在严格模式下,不能定义名为 eval 和 arguments 的变量,否则会导致语法错误。函数不能以 eval 或 arguments 作为名称;函数的参数不能叫 eval 或 arguments。
以下的所有尝试将引起语法错误:
'use strict'
eval = 17
arguments++
++eval
var obj = { set p(arguments) {} }
var eval
try {
} catch (arguments) {}
function x(eval) {}
function arguments() {}
var y = function eval() {}
var f = new Function('arguments', "'use strict'; return 17;")第二,严格模式下,参数的值不会随 arguments 对象的值的改变而变化。在正常模式下,对于第一个参数是 arg 的函数,对 arg 赋值时会同时赋值给 arguments[0],反之亦然(除非没有参数,或者 arguments[0] 被删除)。严格模式下,函数的 arguments 对象会保存函数被调用时的原始参数。arguments[i] 的值不会随与之相应的参数的值的改变而变化,同名参数的值也不会随与之相应的 arguments[i] 的值的改变而变化。
function f(a) {
'use strict'
a = 42
return [a, arguments[0]]
}
var pair = f(17)
console.log(pair[0] === 42)
console.log(pair[1] === 17)在函数中尝试重写 arguments 对象会导致语法错误。
function doAdd(num1, num2) {
'use strict'
arguments = [1, 2, 3] // SyntaxError: Unexpected eval or arguments in strict mode
arguments[1] = 10
console.log(arguments[0] + num2)
}
doAdd(1, 2) // 3第三,不再支持 arguments.callee。正常模式下,arguments.callee 指向当前正在执行的函数。这个作用很小:直接给执行函数命名就可以了!此外,arguments.callee 十分不利于优化,例如内联函数,因为 arguments.callee 会依赖对非内联函数的引用。在严格模式下,arguments.callee 是一个不可删除属性,而且赋值和读取时都会抛出异常:
'use strict'
var f = function () {
return arguments.callee
}
f() // 抛出类型错误"安全的" JavaScript
严格模式下更容易写出“安全”的 JavaScript。现在有些网站提供了方式给用户编写能够被网站其他用户执行的 JavaScript 代码。在浏览器环境下,JavaScript 能够获取用户的隐私信息,因此这类 Javascript 必须在运行前部分被转换成需要申请访问禁用功能的权限。没有很多的执行时检查的情况,Javascript 的灵活性让它无法有效率地做这件事。一些语言中的函数普遍出现,以至于执行时检查他们会引起严重的性能损耗。做一些在严格模式下发生的小改动,要求用户提交的 JavaScript 开启严格模式并且用特定的方式调用,就会大大减少在执行时进行检查的必要。
第一,在严格模式下通过 this 传递给一个函数的值不会被强制转换为一个对象。对一个普通的函数来说,this 总会是一个对象:不管调用时 this 它本来就是一个对象;还是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的 this;还是使用 undefined 或者 null 调用函数式 this 代表的全局对象(使用 call、apply 或 bind 方法来指定一个确定的 this)。这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的 JavaScript 环境必须限制的功能的途径。所以对于一个开启严格模式的函数,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined。
严格模式下,函数直接调用时(不使用new调用),函数内部的this表示undefined(未定义),因此可以用call、apply和bind方法,将任意值绑定在this上面。正常模式下,this指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而null和undefined这两个无法转成对象的值,将被忽略。
'use strict'
function fun() {
return this
}
console.log(fun() === undefined)
console.log(fun.call(2) === 2)
console.log(fun.apply(null) === null)
console.log(fun.call(undefined) === undefined)
console.log(fun.bind(true)() === true)// 正常模式
function fun() {
return this
}
fun() // window
fun.call(2) // Number {2}
fun.call(true) // Boolean {true}
fun.call(null) // window
fun.call(undefined) // window正常模式下,函数内部的 this 可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。
这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加new,这时this不再指向全局对象,而是报错。
function f() {
'use strict'
this.a = 1
}
f() // 报错,this 未定义所有超时执行的代码(函数)都会在全局作用域中的一个匿名函数中运行,因此函数中的 this 值在非严格模式下始终指向 window,而在严格模式下是 undefined。如果给 setTimeout()提供了一个箭头函数,那么 this 会保留为定义它时所在的词汇作用域。
如何避免 JavaScript 中的全局变量污染?
- 立即执行函数
- 严格模式
- 模块化
第二,在严格模式中再也不能通过广泛实现的 ECMAScript 扩展“游走于”JavaScript 的栈中。在普通模式下用这些扩展的话,当一个叫 fun 的函数正在被调用的时候,fun.caller 是最后一个调用 fun 的函数,而且 fun.arguments 包含调用 fun 时用的形参。这两个扩展接口对于“安全”JavaScript 而言都是有问题的,因为他们允许“安全的”代码访问"专有"函数和他们的(通常是没有经过保护的)形参。如果 fun 在严格模式下,那么 fun.caller 和 fun.arguments 都是不可删除的属性而且在存值、取值时都会报错:
function restricted() {
'use strict'
restricted.caller // 抛出类型错误
restricted.arguments // 抛出类型错误
}
function privilegedInvoker() {
return restricted()
}
privilegedInvoker()第三,严格模式下的 arguments 不会再提供访问与调用这个函数相关的变量的途径。在一些旧时的 ECMAScript 实现中 arguments.caller 曾经是一个对象,里面存储的属性指向那个函数的变量。这是一个安全隐患,因为它通过函数抽象打破了本来被隐藏起来的保留值;它同时也是引起大量优化工作的原因。出于这些原因,现在的浏览器没有实现它。但是因为它这种历史遗留的功能,arguments.caller 在严格模式下同样是一个不可被删除的属性,在赋值或者取值时会报错:
'use strict'
function fun(a, b) {
'use strict'
var v = 12
return arguments.caller // 抛出类型错误
}
fun(1, 2) // 不会暴露 v(或者 a,或者 b)为未来的 ECMAScript 版本铺平道路
第一,在严格模式中一部分字符变成了保留的关键字。这些字符包括 implements、interface、let、package、private、protected、public、static 和 yield。在严格模式下,你不能再用这些名字作为变量名或者形参名。
第二,严格模式禁止了不在脚本或者函数层面上的函数声明。在浏览器的普通代码中,在“所有地方”的函数声明都是合法的。这并不在 ES5 规范中(甚至是 ES3)!这是一种针对不同浏览器中不同语义的一种延伸。未来的 ECMAScript 版本很有希望制定一个新的,针对不在脚本或者函数层面进行函数声明的语法。在严格模式下禁止这样的函数声明对于将来 ECMAScript 版本的推出扫清了障碍:
'use strict'
if (true) {
function f() {} // !!! 语法错误
f()
}
for (var i = 0; i < 5; i++) {
function f2() {} // !!! 语法错误
f2()
}
function baz() {
// 合法
function eit() {} // 同样合法
}上面代码在if代码块和for代码块中声明了函数,ES5 环境会报错。
注意,如果是 ES6 环境,上面的代码不会报错,因为 ES6 允许在代码块之中声明函数。
严格模式下有哪些限制?
- 变量必须声明后在使用
- 函数的参数不能有同名属性, 否则报错
- 不能使用 with 语句
- 不能对只读属性赋值, 否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的数据, 否则报错
- 不能删除变量 delete prop, 会报错, 只能删除属性 delete global[prop]
- eval 不会在它的外层作用域引入变量
- eval 和 arguments 不能被重新赋值
- arguments 不会自动反映函数参数的变化
- 不能使用 arguments.caller
- 不能使用 arguments.callee
- 禁止 this 指向全局对象
- 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
- 增加了保留字(比如 protected、static 和 interface)
执行代码求输出,并说明为什么,严格模式下输出有变化吗,为什么
var a = function () {
this.b = 3
}
var c = new a()
a.prototype.b = 9
var b = 7
a()
console.log(b)
console.log(c.b)