关于
本文由 [WowBar][WowBar] 团队首发于 [GitHub][GitHub]
作者: yvongyang
-
目录
-
语句和表达式
JavaScript 中表达式和语句的主要区别在于一条语句执行一个动作,一个表达式产生一个值。意思是一个表达式执行后一定会生成一个值,而语句不一定会产生值。语句主要是用来执行动作,程序就是由一系列语句组成。
例如:// 表达式 name 1 + x getNames() // 语句 var name = 'yang'; function getNames() {} var foo = getNames() {};
接下来的内容里不会介绍表达式,只是列出来表达式的分类,语句部分会分别介绍语句的用法和示例,如果对于表达式和语句的内容比较清楚的可以直接跳到本章最后一部分——表达式和语句的比较。
-
表达式
表达式分为基本的表达式(包括基本关键字),还有左值表达式以及运算符。
1. 基本表达式
- this关键字
- 字面量(null,布尔值字面量,数字字面量,字符串字面量)
- 初始化字面量(数组字面量[],对象字面量{},正则表达式字面量
/ab+c/i
) - 函数表达式
- 类表达式
- 分组操作符
()
- 模板字面量 `..${...}..`
2. 左值表达式
- 属性访问符
- new
- 元属性:new.target
- super
- 函数调用
- 参数列表(arguments, ...arguments)
- import
3. 运算符
- 一元
delete, void, typeof, +, -, ~, ! - 算术
+, -, /, *, %, A++, A--, ++A, --A - 比较
<, >, <=, >=, in, instanceof,==
,===
,!==
,!===
- 条件
condition ? ifTrue : ifFalse - 赋值
=, -=, +=, *=, /=, &=, |=, 解构赋值如[a, b] = [1, 2]、{a, b} = {a: 1, b: 2}等 - 逗号
, - 位移,二进制,二元逻辑
<<, >>, >>>; &, ^, |; &&, ||等
-
语句
语句分为声明语句、流程控制语句和其他语句。
其中,流程控制语句分为基本流程控制语句、迭代语句、跳转语句和条件语句。具体如下。1. 声明语句
-
1.1 变量声明
1.1.1 var 声明
声明一个变量,并可以地将其初始化为一个值
var a; var a = 2; var a = 2, b = 3; // 多个变量的初始化
var 声明的变量是可以提升的,提升意味着无论变量实际在哪里声明的,都会被当成在当前作用域顶部声明的变量。看下面示例 4。根据示例 1,2,3 显示的情况,建议始终声明变量,无论它们在函数还是全局作用域内。
var 声明的函数表达式不能提升。对于未声明的变量,可以用
typeof
检测其是否存在且不会报错。// 示例 1: 声明的变量的作用域在其声明位置的上下文中,而未声明变量是全局的; // 建议始终声明变量,无论它们是否在函数还是全局作用域内 function x() { y = 1; // 在严格模式(strict mode)下会抛出 ReferenceError 异常 var z = 2; } x(); console.log(y); // 打印 "1" console.log(z); // 抛出 ReferenceError: z 未在 x 外部声明 // 示例 2:声明的变量在任何代码执行前创建(会被提升),未声明变量只有在执行赋值操作时被创建; console.log(a); // 抛出 ReferenceError。 console.log('still going...'); // 永不执行。 console.log(a); // 打印 "undefined" 或 ""(不同浏览器实现不同)。 var a; console.log('still going...'); // 打印 "still going..."。 // 示例 3:声明的变量是它所在上下文环境的不可配置属性,非声明变量是可配置的(如可被删除) var a = 1; b = 2; delete this.a; // 在严格模式(strict mode)下抛出TypeError,其他情况下执行失败并无任何提示。 delete this.b; console.log(a, b); // 抛出ReferenceError。 // 'b'属性已经被删除。 // 示例 4: 变量提升 var x = y, y = 'A'; console.log(x + y); // undefinedA // 实际会被转换为: var x; var y; x = y; y = 'A';
1.1.2 let 声明
声明一个块级作用域的变量,并可以将其初始化。
let x; let x = 1; let x = 1, y = 2;
与 var 关键字声明变量的不同点在于:
-
var
声明的变量只能是全局或者整个函数块的,let/const
声明的变量只在其声明的块或子块中使用;(示例 1) -
let/const
不会在全局声明时创建window
对象的属性,而 var 会。(示例 2) -
let/const
在同一个块作用域或函数中不能重复声明(会报错),var
可以;(示例 3,4) -
var
声明的变量会被初始化为undefined
,let/const
声明的变量直到它们的定义被执行时才会初始化。量会被初始化为undefined
,let/const
声明的变量直到它们的定义被执行时才会初始化。
// 示例 1 function varTest() { var x = 1; { var x = 2; // 同样的变量! console.log(x); // 2 } console.log(x); // 2 } function letTest() { let x = 1; { let x = 2; // 不同的变量 console.log(x); // 2 } console.log(x); // 1 } // 示例 2 var x = 'global'; let y = 'global'; console.log(this.x); // "global" console.log(this.y); // undefined // 示例 3 if (x) { let foo; let foo; // SyntaxError thrown. } // 示例 4:case 没用 `{}` 包裹起来没形成块作用域,所以两个 `foo` 会在同一个块中被声明,所以报错。 let x = 1; switch(x) { case 0: let foo; break; case 1: let foo; // SyntaxError for redeclaration. break; } // 示例 5 function do_something() { console.log(bar); // undefined console.log(typeof foo); // ReferenceError,typeof也不安全 var bar = 1; let foo = 2; } // 示例 6 function go(n) { // n here is defined! console.log(n); // Object {a: [1,2,3]} for (let n of n.a) { // ReferenceError console.log(n); } } go({a: [1, 2, 3]});
1.1.3 const
声明一个块作用域中的变量,并必须初始化一个值。与 let 用法基本相同,除了声明的变量的值不能被改变。
let a = 1; a = 2; console.log(a); // 2 const c = 1; c = 2; // Uncaught SyntaxError: Invalid or unexpected token
-
-
1.2 函数声明
每个函数都是一个 Function 对象,与其他对象的区别在于可被调用;
若函数没有 return 语句,则返回 undefined;
函数是值传递方式(对象是引用传递);
Es6 开始,严格模式下,块里的函数作用域为这个块。非严格模式下的块级函数不要用。
-
定义函数的方式有3种:
函数声明: 普通函数声明,生成器函数声明
构造函数: 普通的构造函数 Function, 生成器构造函数 GeneratorFunction。(不推荐构造函数的方式定义函数,函数体为字符串,会引起其他问题)
函数表达式: 函数表达式,函数生成器表达式,箭头函数表达式
写法示例:
// 函数声明定义函数
function getName(name1, name2, ...) {
// 语句
}
// 构造函数定义函数
var getName = new Function('name1', 'name2', 'return "myName:" + name1');
getName('yang'); // "myName:yang"
// 函数表达式定义函数
var getName = function(name1, name2) {
return 'myName:' + name1;
}
// 函数表达式
(function bar() {})
函数声明和表达式区别:
1. 最主要的区别在于函数表达式可以省略函数名称,就是创建匿名函数;
2. 函数表达式未省略函数名称,函数名只能在函数体内用,函数声明的函数名可以在其作用域内被使用;
2. 函数声明可以提升,函数表达式不可以提升,所以表达式不能在调用之前使用;
3. 函数表达式可被用作 IIFE(即时调用的函数表达式)。
var y = function x() {};
alert(x); // throws an error
// IIFE: 函数只使用一次时调用
(function() {
// 语句
})();
函数表达式 name 属性:
被函数表达式赋值的变量有 name 属性,如果把这个变量赋值给另一个变量,name 属性值也不会改变。
// 匿名函数:name属性的值就是被赋值的变量的名称(隐藏值)
var func = () => {}
// func.name
// "func"
// 非匿名函数:那name属性的值就是这个函数的名称(显性值)
var funb = function haha() {}
// funb.name
// "haha"
var fund = func;
// fund.name
// "func"
1.2.1 function
// 不同引擎中最大的传参数量不同 function name(param1, param2, ...) { // 语句 }
1.2.2 函数生成器声明 function*
定义一个生成器函数,返回一个 Generator 对象。
Generator 对象:由 generator function 返回的对象,符合可迭代协议和迭代器协议。
function *gen() {
// 语句
yield 10;
x = yield 'foo';
yield x;
}
生成器函数在执行时能暂停,后面又能从暂停处继续执行;
调用一个生成器函数并不能马上执行它里面的语句,而是返回一个这个生成器的迭代器对象;
当迭代器的 next() 方法被调用时,其内的语句会执行到第一个后续出现 yield 的位置为止,yield 后面紧跟迭代器要返回的值。
next() 返回一个对象,包含两个属性:value 和 done,value 表示本次 yield 表达式的返回值,done 为布尔类型,表示生成器是否已经执行完毕并返回。
若在生成器函数中调用 return 语句时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 done 为 true,return 后面的值会作为当前调用 next() 返回的 value 值。
function* yieldAndReturn() {
yield "Y";
return "R";//显式返回处,可以观察到 done 也立即变为了 true
yield "unreachable";// 不会被执行了
}
var gen = yieldAndReturn()
console.log(gen.next()); // { value: "Y", done: false }
console.log(gen.next()); // { value: "R", done: true }
console.log(gen.next()); // { value: undefined, done: true }
yield* 表示将执行权移交给另一个生成器函数(当前生成器暂停执行),调用 next() 方法时,如果传入了参数,那么这个参数会传给上一条执行的 yield 语句左边的变量:
function* anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}
function* generator(i){
yield i;
yield* anotherGenerator(i);// 移交执行权
yield i + 10;
}
var gen = generator(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
console.log(gen.next().value); // 20
生成器函数不能当作构造器使用,否则会报错。
function*表达式
与 function*声明
有相似的语法,唯一区别在于 function*表达式
可以省略函数名。
var x = function*(y) {
yield y * y;
};
1.2.3 async function
定义一个返回 AsyncFunction 对象的异步函数。
异步函数指通过事件循环异步执行的函数,会通过一个隐式的 Promise 返回结果。
Js 中每个异步函数都是 AsyncFunction 对象,该对象不是全局对象,需要用Object.getPrototypeOf(async function(){}).constructor
获取async function name(param1, param2, ...) { // 语句 }
可以包含 await 指令,await 会暂停异步函数的执行,并等待 Promise 执行,然后继续执行异步函数,并返回结果。
await 只能在异步函数中使用,否则会报错。
async/await 是为了简化使用多个 Promise 时的行为,就像是结合了 generators 和 promises。
使用 async 函数重写 promise 链:
// Promise
function getProcessedData(url) {
return downloadData(url) // 返回一个 promise 对象
.catch(e => {
return downloadFallbackData(url) // 返回一个 promise 对象
})
.then(v => {
return processDataInWorker(v); // 返回一个 promise 对象
});
}
// Async:return 时,async function 的返回值将被隐式地传递给 Promise.resolve。
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}
-
1.3 类声明
ES6 中的类跟其他语言中的类类似,是基于原型继承的。不过 ES6 中的类是基于已有自定义类型的语法糖,typeof 检测类可以发现为 function。
// 简单的类声明 class PersonClass { constructor(name) { this.name = name; } sayName() { console.log(this.name); } } // 自定义类型实现上述代码 function PersonType(name) { this.name = name; } PersonType.prototype.sayName = function() { console.log(this.name); }
上述例子可以看出,类中的构造函数实际相当于自定义类型的 PersonType 函数,类中的 sayName 方法是构造函数原型上的方法。
定义类的两种形式:类声明 和 类表达式。
类声明和类表达式的代码都是强制严格模式的。
和函数表达式一样,类表达式也可以省略类名。如果不省略类名,则类表达式中的类名只能在类体内部使用。后续会单独讲讲类。
2. 流程语句
-
2.1 基本语句
2.1.1 块语句
组合0或多个语句, 可以与label一起用。
{语句组合} 或 标签标识符: {语句组合}
块语句示例:
示例 2 不会报错,因为块级作用域的存在,并且输出的是 1。// 示例 1 var a = 1; { var a = 2; } console.log(a); // Output:2 // 示例 2 const a = 1; { const a = 2; } console,log(a); // Output:1 // 示例 3 label: { const a = 1; }
块语句返回值示例:
块返回的值为块中最后一条语句的返回值,不过因为语句的值获取不到,所以了解即可。var a; function b() {return 'yang';} try { throw 'haha'; } catch(e) { } // Output: undefined var a; function b() {return 'yang';} // Output: ƒ b() {return 'yang';}
2.1.2 空语句
不会执行任何语句
;
空语句示例:
// 跟 for 循环一起的空语句(空语句最好写注释以防混淆) for (let i = 0; i < 5; i++) /* Empty statement */; // if语句 if (one); // do nothing else if (two); // do nothing else all();
-
2.2 迭代语句
2.2.1 while/do...while
while (condition) statement // 想执行多行语句可用块语句 do statement // 想执行多行语句可用块语句 while (condition);
while
可在某个condition
(条件表达式)值为真的前提下,执行循环直到表达式值为false;do...while
执行指定语句的循环直到condition
(条件表达式)值为 false,与while
语句区别在于在执行statement
后检测condition
,所以statement
至少执行一次。两者差别示例:
var i = 1; do { console.log('do..while', i); i++; } while (i < 1); // 输出: // "do...while" // 1 var j = 1; while (j < 1) { console.log('while', j); j++; } // 没有输出
2.2.2 for/for...of/for...in/for await...of
1.for: 创建循环,含三个可选的表达式,表达式包围在圆括号中并由分号分割,后跟一个在循环中执行的语句(通常是一个块语句,即用
{}
包裹起来的语句)。// initialization 为一个表达式(包含赋值表达式)或者变量声明,若没有任何语句要执行,则使用空语句 `(;)` for ([initialization]; [condition]; [final-expression]) statement
2.for...of: 循环遍历可迭代对象(Array, Map, Set, String, TypedArray, arguments 对象等)要迭代的值。
for (variable of iterable) { //statements }
3.for...in: 以任意顺序迭代对象的可枚举属性。(除 Symbol 以外)
for (variable in object) statement
4.for await...of: 在异步或同步可迭代对象上创建一个迭代循环,为每个不同属性的值执行语句。
for await (variable of iterable) statement
for :
如果省略了中间可选的条件表达式(condition
块),则必须确保在循环体内跳出(break 语句
),不然会陷入死循环。
如果省略所有表达式,则确保跳出循环并且修改增量,使break语句
在某条件下为 true.(见示例 2)// 示例 1 var arr = []; for (var i = 0; i < 9; i++) { arr.push(function() { console.log(i); }); } console.log(arr.forEach(item => console.log(item()))); // Output: // [9, 9, 9, 9, 9, 9, 9, 9, 9] // 示例 2 var i = 0; for (;;) { if (i > 3) break; console.log(i); i++; } // 示例 3 for (var i = 0; i < 9; i++); console.log(i); // 9
for...of:
// 迭代 Array let iterable = [10, 20, 30]; for (let value of iterable) { value += 1; console.log(value); } // Output: // 11 // 21 // 31 // 迭代 String let iterable = 'boo'; for (let value of iterable) { console.log(value); } // b // o // o // 迭代 Map let iterable = new Map([['a', 1], ['b', 2]]); for (let [key, value] of iterable) { console.log(value); } // 1 // 2
for...in:
for...in 循环只遍历可枚举属性,不可枚举属性不会遍历,例如 String 的 indexOf() 方法,或者 Object.toString() 方法。
通常,在迭代过程中最好不要在对象上进行添加、修改或者删除属性的操作,因为不能保证这些被修改的属性能被访问到。Object.prototype.objCustom = function() {}; Array.prototype.arrCustom = function() {}; let iterable = [3, 5, 7]; iterable.foo = 'hello'; for (let i in iterable) { console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom" } for (let i in iterable) { if (iterable.hasOwnProperty(i)) { console.log(i); // logs 0, 1, 2, "foo" } } for (let i of iterable) { console.log(i); // logs 3, 5, 7 }
注意:
- for...in 访问 Array 时不一定会按次序访问元素,这是依赖执行环境的,而且访问的是数组的索引,除了索引外还包括其他的属性以及继承的属性;
- for...of 语句遍历可迭代对象要迭代的数据,所以用它来遍历 Array 里的值更好;
- for...in 遍历可枚举属性时,若只考虑对象自身属性,不包含原型,可以用
getOwnPropertyNames()
或者hasOwnProperty()
来确定是否是对象自身属性。
for await...of:
异步生成器(或迭代异步可迭代对象)已经实现了异步迭代器协议,用for await...of
循环:async function* asyncGenerator() { var i = 0; while (i < 3) { yield i++; } } (async function() { for await (num of asyncGenerator()) { console.log(num); } })(); // 0 // 1 // 2
-
2.3 条件语句
2.3.1 if
条件判断
if (condition) statement1 [else if (condition) statement2] [else statement3] //中括号表示可选
2.3.2 switch
评估一个表达式,若表达式的值与 case 子句匹配则执行 case 子句相关联的语句。
switch (expression) { case value1: // 当 expression 的结果与 value1 匹配时,执行此处语句 [break;] ... [default: // 如果 expression 与上面的 value 值都不匹配,执行此处语句 [break;]] // 中括号表示可选
-
2.4 跳转语句
2.4.1 break 语句
中止当前循环(或 switch 语句 或 label 语句),直接执行被中止语句后面的语句。
break [label];
label (可选)—标签相关标识符,如果 break 语句不在一个循环或 switch 语句中,则该项是必须的。// 示例 1: 循环中的 break 语句 var i = 0; while (i < 6) { i += 1; if (i == 3) break; console.log(i); } // Output: // 1 // 2 // 示例 2: break 语句和被标记的块语句 outer_block: { inner_block: { console.log('1'); break outer_block; } console.log ('haha') //被跳过 }
2.4.2 continue 语句
终止执行当前(或标签)循环的语句,直接执行下一个迭代循环。
continue [label];
与
break
语句的区别是,continue
并不会终止循环的迭代:
在 while 循环中,控制流跳转回条件判断;
在 for 循环中,控制流跳转到更新语句。// 示例 1: 循环中的 continue 语句 var i = 0; while (i < 6) { i += 1; if (i == 3) continue; console.log(i); } // Output: // 1 // 2 // 4 // 5 // 6 // 示例 2 var a = 0; var b = 8; checkAB: while(...) { checkB: while(...) { continue checkB; //每次都跳到 checkB 开始执行 } }
2.4.3 throw 语句
抛出一个用户自定义的异常。当前函数的执行将被停止(throw之后的语句将不会执行),并且控制将被传递到调用堆栈中的第一个catch块。如果调用函数中没有catch块,程序将会终止。
throw expression;
throw "Error"; // 抛出了一个值为字符串的异常 throw 42; // 抛出了一个值为整数42的异常 throw true; // 抛出了一个值为true的异常
2.4.4 try...catch 语句
标记要尝试的语句块,并指定一个出现异常时抛出的响应。
try { try_statements } [catch (exception_var_1) {}] [catch (exception_var_2) {}] // exception_var_1, exception_var_2 保存 throw 语句指定的值(如 catch(e) 中的 e ), 可以用这个标识符获取抛出的异常信息,只在 catch 子句内部使用。 [finally {}] // 在 try 块和 catch 块之后执行,在下一个 try 声明之前执行,无论是否有异常抛出总是执行
-
可以嵌套一个或更多的 try 语句,如果内部的 try 语句没有 catch 子句,就会进入包裹它的 try 语句的 catch 子句。
try { try { throw new Error("oops"); } catch (ex) { console.error("inner", ex.message); } finally { console.log("finally"); } } catch (ex) { console.error("outer", ex.message); } // Output: // "inner" "oops" // "finally" // "outer" "oops"
- finally 块返回一个值,无论 try 和 catch 块中是否有任何 return 语句,此值都将成为整个 try-catch-finally 的返回值。
// try-catch 中的 return 必须是作为函数的返回值才行,不然会报错(见下面 return 语句)。此中情况下 try-catch 要放在函数中运行。 (function() { try { try { throw new Error('oops'); } catch (ex) { console.error('inner', ex.message); throw ex; } finally { console.log('finally'); return; } } // 因为在 finally 中 return,所以 `oops` 不会抛到外层 catch (ex) { console.error('outer', ex.message); } })(); // Output: // inner oops // finally // undefined // 整个函数的返回值
-
2.4.5 return 语句
终止函数的执行,并返回一个指定的值给函数的调用者。
return [[expression]]
返回表达式的值,如果忽略表达式的值,则会返回undefined.
在 return 关键字和被返回的表达式之间若使用行终止符(回车换行符,行分隔符和段分隔符)则会自动分号插入,如:
return
a + b;
// 会被自动转换为
return;
a + b;
var a = 1;
var b = 2;
(function() {
return
a + b;
})() // undefined
// 会被自动转换为
(function() {
return a + b;
})() // 3
也可以返回函数表达式,就是高阶函数的定义,高阶函数是一个接收函数作为参数或将函数作为输出返回的函数。
3. 其他语句
3.1 debugger
在程序中调用可用的调试功能,如设置断点。
debugger
3.2 导入/导出:export、import
export: 从模块中导出函数,对象或原始值,以便其他程序可以通过import语句使用。
导出模式分为两种:命名导出 和 默认导出。可以在每一个模块中定义多个命名导出,但是只允许一个默认导出。
导入/导出的模块都是运行在严格模式下。
导出示例:
export let name; // 导出单个属性
export const myName = 'yang'; // 导出常量
export class ClassName {} // 导出类
export default defaultName; // 导出默认属性
export {name1, name2...} // 导出列表
export {defaultName as default, name1 as Wang...} // 重命名导出,将 name1 作为默认属性导出, name2 重命名为 Wang
// 模块重定向,导入指定路径的模块并导出
export * from ...; // 导出指定模块所有导出的属性,除了默认导出值
export {default} from ...; // 导出指定模块中的默认导出值
export {name1, name2...} from ...; // 导出指定模块中某些属性
export {import1 as name1, import2 as name2...} from ...; // 重命名导出指定模块中某些属性
import: 导入由另一个模块导出的绑定。
浏览器中,import 语句只能在声明了 type="module" 的 script 标签中使用。
还有一个类似函数动态的 import(),不需要依赖 type="module" 的 script 标签。
静态 import 更容易从代码静态分析工具和 tree shaking 中受益,动态 import() 则在按需加载模块时有用。
导入示例:
import * as names from 'export.js'; // 导入整个模块内容,使用 names 模块名称作为命名空间
import {myName, ClassName} from 'export.js'; // 导入多个接口
import name from 'export.js'; // 导入默认接口(即用 export default 导出的接口)
import defaultName, {name1, newName as name2} from 'export.js'; // 同时导入默认接口和多个其他接口,并重命名其中某些接口
import defaultName, * as names from 'export.js'; // 同时导入默认接口和多个其他接口,其他接口全部导入并重命名为 names
import 'export.js'; // 导入的模块作为副作用导入(只运行模块中的全局代码),不导入模块中的任何接口。
var promises = import('export.js'); // 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise。
promises.then((module) => {})
3.3 label
在语句前加个可以引用的标识符,可以和 break 或 continue 语句一起用。
label: statement
// 标记块,并使用 break
foo: {
console.log('face');
break foo;
console.log('this will not be executed');
}
console.log('swap');
// for 循环中使用标记
var str = "";
loop1:
for (var i = 0; i < 5; i++) {
if (i === 1) {
continue loop1;
}
str = str + i;
}
console.log(str); // '0234'
目前在非严格模式下,可以对函数声明进行标记,但是严格模式下不可以。生成器函数不论在什么模式下都不能被标记。
L: function F() {}
'use strict';
L: function F() {}
// VM170:2 Uncaught SyntaxError: In strict mode code, functions can only be declared at top level or inside a block.
L: function* F() {}
// VM175:1 Uncaught SyntaxError: Generators can only be declared at the top level or inside a block.
3.4 with
with 语句(不推荐,了解即可),用于扩展语句的作用域链。
在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。with (expression) { statement }
示例:
var a, x, y;
var r = 10;
var Math = {};
with (Math) {
a = PI * r * r;
x = r * cos(PI);
y = r * sin(PI / 2);
}
// Uncaught ReferenceError: PI is not defined
// 因为作用域中存在 Math 变量,所以先查找该变量中的 Math 对象是否有 PI 属性,发现没有所以报错。
// 'with' 语句将变量 Math 对象添加到作用域链的顶端,在查找变量值 PI 时,会在指定的对象 Math 中查找,发现没有所以报错。with 查找对象时,会先从当前作用域中查找,所以查找起来将会很慢。而且调试起来也会麻烦。
-
表达式语句
任何表达式都可以成为语句,就是说在任何需要写语句的地方,都可以写表达式,这样的语句叫做表达式语句,表达式语句是一种特殊的语句。反过来,我们不能在写表达式的地方写语句。
下图是 ecma262 规范中 If 语句的语法。其中有 Statement 的地方都可以使用 Expression 即表达式,例如下方示例中的 callback 函数调用表达式,就是替代了原来的语句,也是表达式语句。
示例:
// callback 为表达式语句,是一种特殊的语句 if (true) callback()
-
比较
1. 如何区分表达式和语句呢?
1.看是否产生值判断,对表达式求值一定会返回值,对语句求值未可能有返回值也可能没有返回值;
2.看后面是否有分号,有分号的一定是语句,没有分号的可能是表达式也可能是语句。下面两个例子,第一个能成功 log 的原因在于 if 语句括号里应该为表达式,而 true 是表达式中的布尔值字面量。第二个
var a = 0
是声明语句而不是表达式,没有返回值,所以会报错。if (true) { console.log('Hi'); } // 输出: // Hi if (var a = 0) { console.log('Hi'); } // 输出: // Uncaught SyntaxError: Unexpected token 'var'
2. 相似的表达式和语句
2.1 if 语句和条件表达式
if 语句和条件表达式表示的含义一样,只是一个是语句,一个是表达式会返回值而已。
var x; var y = -1; // if 语句 if (y >= 0) { x = y; } else { x = -y; } // 条件表达式 x = (y >= 0 ? y : -y); // 括号不是必须的,加上括号更容易阅读
2.2 函数声明和函数表达式
函数表达式与函数声明拥有几乎相同的语法,但是有以下区别:
- 在函数表达式中可以省略函数名称,省略函数名称即为匿名函数;函数声明中则不能省略函数名;
- 函数表达式可以用作 IIFE (即时调用函数表达式),函数声明不能用作 IIFE。
// 函数表达式:省略函数名 function () {} // 函数表达式:未省略函数名 // 写法与函数声明完全一致 function foo() {}
未省略函数名的函数表达式与函数声明没有区别,但是作用不同:函数表达式产生值,即函数;函数声明导致动作,创建一个变量,其值为函数。
未省略函数名的函数表达式中的函数名只能在函数内部自调用,在函数外部调用会报错。示例:
var outSideFuncName = function inSideFuncName(x) { return x <= 1 ? 1 : x * inSideFuncName(x - 1); } outSideFuncName(5); // Output: 120 > outSideFuncName // Output: // ƒ inSideFuncName(x) { // return x <= 1 ? 1 : x * inSideFuncName(x - 1); // } > insideFuncName // Output: // Uncaught ReferenceError: inSideFuncName is not defined
2.3 对象字面量表达式和块语句
我们知道对象字面量是表达式,它的写法为
{key: value}
形式,块语句是用{}
包裹的语句。当块里包含的是label
语句且label
语句后面是表达式语句的时候,对象字面量表达式和块语句的写法可能会完全一致。
示例:{ foo: bar(3, 5) }
上面的例子中既是对象字面量,又可以说是块语句。作为块语句而言,块里则是标签为 foo 的 label 语句,标签后的语句为一个函数调用表达式,根据前面对表达式语句的定义, 可以知道
bar(3, 5)
是一个表达式语句。所以在程序中我们看到的
{}
有可能是字面量,有可能是块语句,根据上下文情况区分。看下面的例子:> [] + {} "[object Object]" > {} + [] 0
为什么两个结果不一致呢?原因就在于前面的
{}
被作为字面量计算的,后面的是作为块语句计算。例中还涉及到隐式转换的问题,此处埋个伏笔,后续我会单独出一篇文章讲解。2.4 表达式中的逗号和语句中的分号
在 JavaScript 中,语句是用分号隔离,例如
foo(); bar()
;表达式可用逗号隔离,例如foo(), bar()
,两个表达式都会执行,只是会返回后面的表达式的值。> "a", "b" 'b' > var x = ("a", "b"); > x 'b'
3. 使用对象字面量和函数表达式作为语句
我们已经知道了表达式可以放在任何需要语句的地方,这种即表达式语句。对于某些表达式与语句没有区别的情况,如 2.3 中的对象字面量和块,2.4 中的函数表达式和函数声明,如何区分是表达式还是语句呢?一般情况下,是根据出现在表达式上下文还是语句上下文中区分,但是有一种情况例外,就是表达式语句。
因为表达式语句是一种特殊的语句,所以其上下文为语句上下文,这种情况中的
{}
会被当作块处理,function
开头的语法会被当作函数声明。为了避免歧义,JavaScript语法禁止表达式语句使用{
和function
开头。如果一定要用这两个开头的表达式,并且让它们仅作为表达式处理,则可以将表达式放到()
分组操作符中,使它们出现在表达式的上下文中,并且不会改变表达式结果。(Expression)
——分组操作符,由圆括号包裹表达式和子表达式,返回执行表达式的结果。还有另一种方式确保在表达式上下文中解析表达式,就是使用一元操作符,如
!
或+
等。但是与()
的不同点在于一元操作符会改变表达式的结果。见下方立即调用函数表达式中的示例。下面看看
eval
和立即调用函数表达式中的应用。-
eval
eval 是在语句上下文中解析其参数,所以例子中上面的示例将
{}
解析为块,所以里面为 label 语句,因而输出为123
。 加上括号后,{}
及其里面的内容的上下文为表达式,所以{foo: 123}
被视为字面量表达式,因而输出为{foo: 123}
。> eval("{foo: 123}"); 123 > eval("({ foo: 123 })") {foo: 123}
-
立即调用函数表达式(IIFEs)
// 立即调用函数表达式 > (function () { return "hello" }()) 'hello' > (function () { return "hello" })() 'hello' // 省略括号之后的立即调用函数表达式 > function () { return "hello" }() Uncaught SyntaxError: Function statements require a function name > +function () {console.log('hello')}() hello NaN // 返回 NaN 是因为表达式返回的为 undefined,所以 +undefined 为NaN > void function () {console.log('hello')}() hello undefined // 同理,void undefined 返回 undefined
有一点要注意的是,连续使用 IIFEs 时,要记得加分号,否则会报错。因为后面的 IIFE 会把前面的 IIFE 的结果当作函数调用。
若想省略分号,可以将一元运算符放在立即调用函数表达式前面,因为会有自动分号插入。自动分号插入机制后续我也会讲到,此处埋上第二个伏笔。(function () {}()) (function () {}()) // VM613:1 Uncaught TypeError: (intermediate value)(...) is not a function (function () {}()); (function () {}()) // undefined void function () {}() void function () {}() // undefined
-
参考
https://2ality.com/2012/09/expressions-vs-statements.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators