ECMAScript 严格模式的限制和例外情况

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

前戏

对应协议内容 C The Strict Mode of ECMAScript

JavaScript的严格模式(Strict Mode)是一种为了解决语言的一些不合理或不安全的特性,并为未来语言的发展打下更好基础而引入的可选模式。在严格模式下,某些不安全或易导致错误的代码会被禁用或以更严格的方式处理。

大家都知道严格模式下,有一些限制和例外情况,但是具体有哪些,那么还有比协议更完善的清单吗? 还有谁?

严格模式下的保留关键字

有些词在非严格模式下可能可以用作变量名或函数名,但在严格模式下这样做会引发错误,因为它们被预留以备将来版本的JavaScript可能引入的新特性使用。

清单如下

implements interface let package private protected
public static yield

非严格模式:

javascript 复制代码
let implements = 'implements'; 
let interface = 'interface'; 
let let = 'let'; 
let package = 'package'; 
let private = 'private'; 
let protected = 'protected'; 
let public = 'public'; 
let static = 'static'; 
let yield = 'yield'; 

严格模式

javascript 复制代码
"use strict"
let implements = 'implements'; // Uncaught SyntaxError: Unexpected strict mode reserved word
let interface = 'interface';   // Uncaught SyntaxError: Unexpected strict mode reserved word
let let = 'let';               // Uncaught SyntaxError: Unexpected strict mode reserved word
let package = 'package';       // Uncaught SyntaxError: Unexpected strict mode reserved word
let private = 'private';       // Uncaught SyntaxError: Unexpected strict mode reserved word
let protected = 'protected';   // Uncaught SyntaxError: Unexpected strict mode reserved word
let public = 'public';         // Uncaught SyntaxError: Unexpected strict mode reserved word
let static = 'static';         // Uncaught SyntaxError: Unexpected strict mode reserved word
let yield = 'yield';           // Uncaught SyntaxError: Unexpected strict mode reserved word

但是在现代浏览器,比如最新的edge和chrome , 实际上 let 和 const 做变量名都是会报错的。

Microsoft Edge 版本 125.0.2535.67 (正式版本) (64 位)

chrome 125.0.6422.112(正式版本) (64 位)

不允许使用可能引起混淆的前导零来表示八进制数

严格模式下的代码时,必须禁止使用

何为旧式八进制字面量?旧式的八进制字面量是用零(0)作为前缀,后跟一系列数字(0-7)来表示的。例如:

javascript 复制代码
var octalValue = 0755; // 0755是八进制表示法,等同于十进制的493

在新版的JavaScript中,推荐使用0o作为八进制字面量的前缀,如下所示:复制

javascript 复制代码
var octalValue = 0o755; 

简而言之,在严格模式中,不允许使用可能导致混淆的八进制表示法和非标准的十进制数字前缀零写法。

是不是很抽象,看例子就很好理解了。

例子1: LegacyOctalIntegerLiteral(旧式八进制字面量)制

不被允许的例子(严格模式下) :

javascript 复制代码
"use strict";
console.log(0777); // Uncaught SyntaxError: Octal literals are not allowed in strict mode
                   // 这将抛出错误,因为使用了旧式八进制字面量

在非严格模式下,0777会被解释为八进制数777,但在严格模式下,这样的表示是不允许的,因为它可能导致混淆。

例子2: DecimalIntegerLiteral :: NonOctalDecimalIntegerLiteral(非八进制前导零的十进制数字字面量)

非八进制十进制整数字面量(NonOctalDecimalIntegerLiteral)这个术语可能有些混淆,因为通常十进制整数字面量(DecimalIntegerLiteral)不涉及八进制。但是,根据JavaScript的早期版本,整数字面量可以以0开头,这会被解释为八进制数。

使用0开头的非八进制数字字面量是不被允许的。因此,"非八进制十进制整数字面量"实际上指的是那些以0开头但不是八进制数(即,第一个数字不是0-7范围内的数字)的十进制整数字面量。

实际上,严格模式下对十进制数字前导零的处理:

javascript 复制代码
"use strict";
var num = 08; // 错误:在严格模式下,这是非法的,因为看起来像是八进制,但实际上不是
console.log(0123); // 这同样会抛出错误,因为尽管意图可能是十进制,但前导零让人误以为是八进制
console.log(0);  // 这是允许的,因为单个零既不是八进制也不是试图表示其他数字
console.log(020); // 不允许,即便意图是十进制的20,但前导零加非零数字的组合被视为非法

总结来说,严格模式强调清晰性和一致性,因此去除了可能导致误解的数字表示法,确保代码的意图更加明确

禁止使用 传统的八进制转义序列以及 非八进制的十进制转义序列

旧式的八进制字面量是用零(0)作为前缀,后跟一系列数字(0-7)来表示的。例如:

javascript 复制代码
console.log("\075"); // 输出 "K",因为075是八进制的75,对应的ASCII码是"K"

严格模式的代码时,必须禁止使用

这意味着在严格模式下,不允许使用可能导致混乱的传统八进制转义字符或非八进制数字表示的转义序列,以增强代码的可读性和避免潜在的误解。

是不是很抽象,看例子就很好理解了。

例子1: LegacyOctalEscapeSequence(传统八进制转义序列)

不被允许的例子(严格模式下) :

javascript 复制代码
"use strict";
let str = '\123'; 
// 这将在严格模式下抛出错误,因为使用了传统八进制转义序列
console.log(str);

在上述代码中,\123是尝试使用八进制表示法来表示一个字符,但在严格模式下这是不允许的。

例子2: NonOctalDecimalEscapeSequence(非八进制十进制转义序列)

javascript 复制代码
"use strict";
let anotherStr = '\855'; 
// Uncaught SyntaxError: \8 and \9 are not allowed in strict mode.  
// 实际上,任何非八进制前导零的数字序列在严格模式下都应该避免,以免造成误解
console.log(anotherStr);

在这个例子中,虽然\855并非传统八进制(因为八进制数字范围是0-7),但这种形式可能引起混淆,最好的做法是直接使用Unicode转义或其他明确的转义方式,如\uffff。

赋值语句左侧的限制

赋值语句左侧不能引用一个无法解析的引用

严格模式下,对未声明的标识符或者其他无法解析的引用进行赋值操作时,不会自动在全局对象上创建相应的属性。具体来说,当在严格模式代码中执行简单的赋值操作时,赋值语句左侧(LeftHandSideExpression)不能引用一个无法解析的引用。如果发生这种情况,将会抛出一个ReferenceError异常(根据6.2.5.6章节的规定)。

严格模式

javascript 复制代码
"use strict"
identifierX = '10'  // Uncaught ReferenceError: identifierX is not defined

非严格模式

javascript 复制代码
identifierX = '10'  // '10'  全局对象创建 identifierX 属性

赋值语句的左侧也不能引用具有以下属性的属性

  • 数据属性,其特性值包含 { [[Writable]]: false },意味着该属性是不可写的。
  • 访问器属性(getter/setter),其特性值包含 { [[Set]]: undefined },表明没有设置setter方法,因此不能被赋值。
  • 对象的一个不存在的属性,且该对象的[[Extensible]]内部插槽为false,意味着该对象是不可扩展的。

在上述这些情况下,将会抛出一个TypeError异常(根据13.15章节的规定)。

下面的三个示例依次对应上面的三种情况。

javascript 复制代码
"use strict"
var objA = {};
Object.defineProperty(objA,"a", { writable: false});
objA.a = 10;  // Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'

var objB = {};
Object.defineProperty(objB,"a", { set: undefined});
objB.a = 10; // Uncaught TypeError: Cannot set property a of #<Object> which has only a getter

var objC = {};
Object.preventExtensions(objC);
objC.a = 10; // Uncaught TypeError: Cannot add property a, object is not extensible

标识符引用 "eval" 或 "arguments" 的限制

赋值操作

不能直接对eval或arguments这两个关键字进行赋值操作

javascript 复制代码
"use strict";
// 尝试给 "eval" 赋值
// Uncaught SyntaxError: Unexpected eval or arguments in strict mode
eval = function() { console.log("This is not allowed"); };


// 尝试给 "arguments" 赋值
// Uncaught SyntaxError: Unexpected eval or arguments in strict mode
arguments = [1, 2, 3];

在严格模式下,以上两行代码分别尝试给eval和arguments赋值,这都是不允许的。

更新表达式 UpdateExpression

尝试通过更新表达式(UpdateExpression) 修改以eval或arguments也是不允许的。虽然这在技术上不直接违反上述规则,但可能导致混淆或不预期的行为,尤其是在涉及到实际的eval函数或arguments对象时。

javascript 复制代码
"use strict";

// 尝试自增 "eval"
eval++;

// 尝试自减 "arguments"
arguments++;

示例3: 一元前缀操作符

javascript 复制代码
"use strict";
// 尝试使用一元前缀递增操作符
// Uncaught SyntaxError: Unexpected eval or arguments in strict mode
++eval;

// 尝试使用一元前缀递减操作符
// Uncaught SyntaxError: Unexpected eval or arguments in strict mode
--arguments;

类似地,直接对eval或arguments使用一元前缀递增或递减操作符也是不被允许的,这将导致SyntaxError或ReferenceError。

在严格模式下编写代码时,应当避免直接修改或重定义eval和arguments,以遵守语言的规范并保持代码的清晰与安全。

函数的arguments对象访问callee会异常

的严格模式(strict mode)下,函数的arguments对象会定义一个不可配置(non-configurable)的访问器属性callee。尝试访问这个callee属性时,会抛出一个TypeError异常。这一规则体现在ECMAScript语言规范的10.4.4.6节中。

例如,下面的代码在非严格模式下可能正常工作:

javascript 复制代码
function factorial(n) {
    if (n <= 1) return 1;
    return n * arguments.callee(n - 1); // 使用arguments.callee递归调用自身
}
console.log(factorial(5)); // 应输出120

然而,在严格模式下,尝试相同的操作将抛出错误:

javascript 复制代码
"use strict";

function factorial(n) {
    if (n <= 1) return 1;
    return n * arguments.callee(n - 1); // 抛出TypeError
}

factorial(5); // 运行时将抛出TypeError

这一改变鼓励开发者采用更清晰的编程实践,比如直接命名函数并使用函数名进行递归调用,而不是依赖于arguments.callee。

arguments对象的数组索引属性值并不会与函数的形式参数动态共享

在严格模式下,arguments对象的数组索引属性值并不会与函数的形式参数动态共享。这一规定体现在ECMAScript语言规范的10.4.4节中。

在非严格模式下,arguments对象和函数的实际参数之间存在一种特殊的联动关系,即修改arguments[i]的值可能会影响到对应位置的参数变量,反之亦然。但这种联动在严格模式下被取消了,提供了更强的数据隔离性和可预测性。

例如,考虑以下非严格模式下的代码:

javascript 复制代码
function test(a) {
  arguments[0] = 2;
  console.log(a); // 输出2,因为a和arguments[0]是共享的
}
test(1);

而在严格模式下,同样的逻辑表现不同:

javascript 复制代码
"use strict";

function test(a) {
    arguments[0] = 2;
    console.log(a); // 输出1,因为a和arguments[0]不再共享
}
test(1);

在严格模式下,即使你修改了arguments[0]的值,它也不会影响到a的值,反之亦然。这种分离使得开发者可以更安全地操作arguments对象,而不必担心意外改变函数参数的原始值,增加了代码的稳定性和可维护性。

严格模式的函数内部, arguments 不能作为赋值表达式的目标

严格模式(strict mode)下,如果为函数创建了一个arguments对象,那么局部标识符arguments与该arguments对象之间的绑定是不可变的。因此,arguments不能作为赋值表达式的目标。这一规则在ECMAScript规范的10.2.11节中有详细说明。

javascript 复制代码
"use strict";

function testFunction(arg1, arg2) {
    console.log(arguments); // 打印初始的arguments对象
    try {
        arguments = {}; // 尝试将arguments重新赋值给一个空对象,这在严格模式下是不允许的
    } catch (e) {
        console.error(e.message); // 打印错误信息
    }
    console.log(arguments); // 再次打印arguments对象,显示它并未被重新赋值
}

testFunction('value1', 'value2');

绑定标识符(BindingIdentifier,即变量名)的字符串值是 "eval" 或 "arguments",则构成语法错误(SyntaxError)

严格模式(strict mode)中,如果一个绑定标识符(BindingIdentifier,即变量名)的字符串值是 "eval" 或 "arguments",则构成语法错误(SyntaxError)。这一规则出现在ECMAScript规范的13.1.1节中。

javascript 复制代码
"use strict";
var eval = 10; // 这将导致SyntaxError,因为变量名不能是 "eval"

let arguments = [] // 这同样会抛出SyntaxError,变量名不能是 "arguments"

var  { eval, arguments } = {}; // 这同样会抛出SyntaxError

eval执行的代码不能在其调用者的变量环境中实例化变量或函数

严格模式下,使用eval执行的代码不能在其调用者的变量环境中实例化变量或函数。相反,会为eval代码创建一个新的变量环境,并在这个新环境中进行声明绑定的实例化(即变量和函数的创建)(这一规则体现在ECMAScript规范的19.2.1节中)

严格模式

javascript 复制代码
// 启用严格模式
'use strict';

// 在全局作用域中声明一个变量
var globalVar = 'I am global';

// 使用 eval 执行一段代码
eval('var localVar = "I am local"; console.log(localVar);');  // 'I am local'

// 尝试访问在 eval 中声明的变量
console.log(localVar); //  undefined

// 尝试访问全局变量
console.log(globalVar); // 'I am global'

非严格模式

javascript 复制代码
// 在全局作用域中声明一个变量
var globalVar = 'I am global';

// 使用 eval 执行一段代码
eval('var localVar = "I am local"; console.log(localVar);');  // 'I am local'

// 尝试访问在 eval 中声明的变量
console.log(localVar); //  'I am local'

// 尝试访问全局变量
console.log(globalVar); // 'I am global'

this的值不会被强制转换

严格模式下,this 的值不会被强制转换为对象。

  • 如果this的值是undefined或null,它们不会被转换为全局对象,而且原始值也不会被包装成对象。
  • 通过函数调用(包括使用Function.prototype.apply和Function.prototype.call方法进行的调用)传递的this值,也不会被强制转换为对象。

这些规则分别在ECMAScript规范的10.2.1.220.2.3.120.2.3.3节中有详细描述

直接调用函数

在非严格模式下,直接调用一个没有被绑定到对象的方法,this通常会默认指向全局对象(在浏览器中是window)。但在严格模式下,this将是undefined。

严格模式

javascript 复制代码
function getAge(){
  "use strict";
  return this.age  // 此时的this是undefined, 不会使用全局对象
}

getAge();
// Uncaught TypeError: Cannot read properties of undefined (reading 'age')

非严格模式

javascript 复制代码
function getAge(){
  return this.age   // this 是 undefined, 找干爹 全局对象
}

getAge();
// undefined

使用apply或call方法

在非严格模式下,即使你传递null或undefined给Function.prototype.apply或Function.prototype.call,它们会被替换为全局对象。但在严格模式中,这些值保持不变。

非严格模式非严格模式

javascript 复制代码
function showContext() {
    console.log(this);
}

showContext.call(null);  //Window  (浏览器环境)

严格模式

javascript 复制代码
function showContext() {
    "use strict";
    console.log(this);
}

showContext.call(null);  // null ,不会被包装

对原始值的处

对于原始值(如字符串、数字、布尔值等),在非严格模式下,当它们作为函数的this值时,会被自动包装成对应的包装对象(如String、Number、Boolean)。但在严格模式下,这种自动包装不会发生,this直接保持为原始值。

非严格模式

javascript 复制代码
// 非严格模式
function logType() {
    console.log(typeof this);
}

logType.call("hello"); // "object"

严格模式

javascript 复制代码
// 严格模式
function logType() {
    "use strict";
    console.log(typeof this);
}

logType.call("hello"); //  "string"

delete尝试删除一个变量、函数参数或函数名的直接引用时,会抛出SyntaxError错误

严格模式中,如果使用delete操作符尝试删除一个变量、函数参数或函数名的直接引用时,会抛出SyntaxError错误。这一规则在ECMAScript规范的13.5.1.1节中有明确规定。

这意味着在严格模式下,以下类型的直接引用使用delete操作是非法的:

  • 变量: 包括使用var、let、const声明的变量。
  • 函数参数: 函数定义中声明的参数列表中的参数。
  • 函数名: 已经定义的函数的名称。
javascript 复制代码
"use strict";

let myVar = 42;
function myFunction() {}

try {
    delete myVar; // 尝试删除变量,抛出SyntaxError
} catch (e) {
    console.error(e.message);
}

try {
    function anotherFunction(arg) {
        delete arg; // 尝试删除函数参数,抛出SyntaxError
    }
    anotherFunction(10);
} catch (e) {
    console.error(e.message);
}

try {
    delete myFunction; // 尝试删除函数名,抛出SyntaxError
} catch (e) {
    console.error(e.message);
}

delete 删除不能删除的属性,抛出 TypeError

严格模式下,如果delete操作符尝试删除一个具有 { [[Configurable]]: false }属性特征的属性,或者该属性因其他原因不可删除时,会抛出TypeError错误。这一规定体现在ECMAScript规范的13.5.1.2节中。

javascript 复制代码
"use strict";

let obj = {};

Object.defineProperty(obj, "fixedProperty", {
    value: "This is fixed",
    configurable: false // 属性被设置为不可配置
});

try {
    delete obj.fixedProperty; // 尝试删除不可配置的属性
} catch (e) {
    console.error(e.message); // Cannot delete property 'fixedProperty' of #<Object>
}

// 尝试删除原型链上的不可配置属性也会导致TypeError
Object.freeze(Object.prototype); // 冻结Object.prototype,使其所有属性变为不可配置
try {
    delete Object.prototype.toString; 
} catch (e) {
    console.error(e.message); // 输出类似TypeError的错误信息
}

在版本 125.0.6422.113(正式版本) (64 位), 抛出了异常,但是并没显示表示是 TypeError

不允许使用 with 语句

在的严格模式下,代码不允许包含WithStatement。如果在严格模式的上下文中出现了WithStatement,将引发一个SyntaxError。这一规定在ECMAScript规范的14.11.1节中有明确阐述。

在非严格模式下,WithStatement可以使用,尽管不推荐:

javascript 复制代码
var obj = { a: 1 };
with (obj) {
  console.log(a); // 输出 1,没有需要写obj.a
}

但在严格模式下,同样的代码会导致SyntaxError:

javascript 复制代码
"use strict";
var obj = { a: 1 };
with (obj) { // 这里会抛出SyntaxError
  console.log(a);
}

严格模式通过禁止WithStatement,鼓励开发者编写更清晰、可预测且易于优化的代码。

catch 参数包含eval或arguments,SyntaxError

严格模式下,如果CatchParameter(catch子句中的参数)出现,并且其BoundNames(绑定的名称集合)中包含eval或arguments,那么将构成一个SyntaxError。这一规则在ECMAScript规范的14.15.1节中有详细描述

javascript 复制代码
"use strict";

try {
    // 尝试执行可能抛出异常的代码
    throw new Error("An error occurred.");
} catch (eval) { // 这里会导致SyntaxError,因为使用了eval作为CatchParameter
    console.log(eval.message);
}

try {
    throw "Another error.";
} catch (arguments) { // 同样,使用arguments作为CatchParameter也会导致SyntaxError
    console.log(arguments);
}

try {
    throw  {}
} catch ({eval, arguments}) { // 同样,使用arguments作为CatchParameter也会导致SyntaxError
    console.log(arguments);
}

形式参数列表 中参数名出现多次,SyntaxError

严格模式中,如果在函数的FormalParameters(形式参数列表)中同一个BindingIdentifier(绑定标识符,即参数名)出现多次,那么这将构成一个SyntaxError。

此外,尝试使用Function构造函数、Generator构造函数或AsyncFunction构造函数来创建具有这样重复参数名的函数也会导致SyntaxError。这一规则分别在ECMAScript规范的15.2.120.2.1.1.1节中有所规定。

javascript 复制代码
"use strict";

// 直接定义函数
function exampleFunc(param, param) { // SyntaxError: param重复
    console.log(param);
}

// 使用Function构造函数
try {
    var func = new Function("param", "param", "console.log(param)"); // SyntaxError
} catch (e) {
    console.error(e.name + ": "  + e.message);
}

try {
    var genFunc = (function* (param, param) { yield param; }); // SyntaxError
} catch (e) {
    console.error(e.name + ": "+ e.message);
}

try {
    var asyncFunc = async function(param, param) { return param; }; // SyntaxError
} catch (e) {
    console.error(e.name + ": " + e.message);
}

函数对象上的caller和arguments属性

  • caller属性 : 它通常用于引用调用当前函数的函数。但在严格模式下,访问或修改caller属性会抛出错误,以防止潜在的信息泄露和不安全的函数引用操作。
  • arguments对象 : 直接修改arguments标识符是不允许的,但是arguments对象本身仍然可用,用于访问函数的实参
javascript 复制代码
function sum(a, b) {
  "use strict";
    // 访问arguments是允许的,这里输出传递给a的值
    console.log(arguments);   // [5, 3]
    // /但直接修改arguments的内容是允许的,如:arguments[0] = 10; 但是a不会随着变化
    arguments[0] = 10;   
    console.log(arguments);   // [10,3]
    console.log(a, b);        // 5 3
    arguments = {}; // 这里尝试修改arguments标识符会抛出错误,因为是严格模式
    return a + b;
}

sum(5, 3);
javascript 复制代码
function outer() {
    'use strict';
    outer.caller   // 异常
}
相关推荐
高木的小天才5 分钟前
鸿蒙中的并发线程间通信、线程间通信对象
前端·华为·typescript·harmonyos
Danta1 小时前
百度网盘一面值得look:我有点难受🤧🤧
前端·javascript·面试
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
dwqqw1 小时前
opencv图像库编程
前端·webpack·node.js
Captaincc2 小时前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚2 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿3 小时前
MCP
前端
layman05283 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝3 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML3 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能