Adamwu1992 / adamwu1992.github.io

My Blog
2 stars 0 forks source link

【JavaScript】语句 vs 表达式 #11

Open Adamwu1992 opened 6 years ago

Adamwu1992 commented 6 years ago

在JavaScript中有两个重要但是容易混淆的语法类别:语句(statements)和表达式(expression)。

弄清楚它们之前的区别是很重要的。表达式可以当做语句来使用,我们称之为表达式语句;但是反过来,语句却不能被当做表达式。

表达式 - Expression

表达式产生一个值

在JavaScript中,表达式是一个结果为一个值的代码片段。表达式可以是任意长度的,但是它们最终只能是一个单个值。

2 + 2 * 3 / 2

myVar

window.history ? useHistory() : noHistoryFallback()

上面出现的每一行都是一个表达式,可以出现在JS代码中任意需要一个值的地方。

表达式不一定会改变状态

例如:

const aa = 1; // 语句 aa表示状态

aa + 1 // 表达式

aa * 10 // 表达式

上面的后两行都是表达式,但是aa的值始终都是一开始的1。

之所以用‘不一定’,是因为函数调用也是表达式,但是函数里可能包含语句从而改变状态:

function foo() {
  aa = 10
}

foo() // 表达式,但是状态aa被改变了

如果要用更加优雅的方式来改变状态,应该这样改写:

function foo() {
  return 10;
}
aa = foo()

// 或者更加通用的方式

function foo(n) {
  return n;
}

aa = foo(10)

这样的代码更加易读和可组合,并且能显式的区分表达式和语句,这也是函数式和声明式JS的基础。

语句 - Statements

语句是函数式编程中令人头疼的东西。大概来说,语句就像一个动作,执行一个操作。

在JS中语句不能被用在需要值的地方。类似的地方有:函数的参数、赋值符号的右边、运算符的操作数、被return返回的值,等等。

以下是JS中全部的语句:

如果你在浏览器的控制台中输入if (true) {9+9},你可以看到控制台中会返回18。但是你仍然不能把这段代码写在JS里需要一个值的地方。这很奇怪,你不期望语句返回任何内容,因为你不能使用它的返回值,即使返回了也是白搭。这就是JS给我们带来的,要学会接受 😩

函数声明 vs 函数表达式 vs 命名函数表达式

函数声明是一个语句:

function getName (func) {
  return func.name;
}

函数表达式是一个表达式(听起来像是废话 😆 ),也就是匿名函数:

getName(function () {}) // ""

命名函数表达式也是一个表达式,类似匿名函数,除了它有一个名字:

getName(function foo () {}) // "foo"

函数声明和命名函数表达式看起来非常类似(根本就是完全一样 😭 ),有一个比较好的方法区分它们:

无论何时,当你在JS中需要一个值的地方声明了一个函数,这个函数将被看做一个值,如果它不能被当做值处理就会抛出错误;

当你在一个顶层作用域声明了一个函数,它将被看做是函数声明

顶层作用域包括脚本(scripts)的顶层,模块(module)的顶层和块(block)的顶层。或者简单点,任何不需要一个值的地方。

if (true) {
  function foo() {} // 块的顶层 函数声明
}

function foo() {}  // 全局脚本的顶层 函数声明

function foo() {
  function bar() {} // 块的顶层 函数声明
}

function foo() {
  return function bar() {} // 返回值 命名函数表达式
}

function foo() {
  return function bar() {
    function baz() {}  // 块的顶层 函数声明
  }
}

立即执行函数

function () {}

(function () {})

上面第一种写法会导致报错,而第二种是合法的。如果把一个匿名函数放到括号内,它会立即将这个匿名函数返回,所以可以在后面加上括号来立即调用这个匿名函数:

(function () {})()

因为funciton () {}既可以被解析成表达式也可以被认为是声明语句,如果要用作立即调用函数,我们需要确保它是在表达式的上下文中被解析的,括号内的内容可以确保是处于表达式的上下文中,所以这种写法也可以通过:

(function() {}())

我们还可以用一些一元运算符来确保表达式的上下文环境:

> +function () { console.log("hello") }()
hello
NaN

> !function () { console.log("hello") }()
hello
true

> void function() {console.log("hello")}()
hello
undefined

使用+或者!会改变原本的返回值,大部分情况下这没什么问题,如果你介意的话,可以使用关键字void

如果是使用括号的话,当连接多个IIFE的时候需要注意了:

(function () {}())
(function () {}())
// TypeError: undefined is not a function

因为括号会被当做表达式,所以JS不会自动插入分号在行末,而是试图把他们连起来解析,就会抛出语法错误。手动插入分号可以避免这个问题:

(function () {}());
(function () {}())
// OK

表达式语句 - Expression Statements

2 + 2; // 语句表达式

foo(); // 语句表达式

你只需要在表达式的最后添加一个分号,或者让JS的自动插入分号生效,就可以转化为语句。2+2或者foo()本身是一个表达式,你可以将它们用在任何需要值的地方,但是上面整个一行(加上分号)算是一个语句,只能出现在上述表达式应该出现的地方。

逗号 vs 分号

分号可以使多个表达式显示在一行。

const a; a = 1; function foo() {}

逗号可以是多个表达式出现在一行,每一个表达式都会被计算,最后一个表达式会被当做结果返回。

1+2, 1+3, 1+4 //4

console.log((1+2, 1+3, function a() {})) // function a() {}

像上面的console.log可以接受多个参数的场景,你可以使用括号将多个表达式包裹起来,告诉编译器这是一个整体。

字面量对象 vs 块表达式

r: 2 + 2

foo()

const foo = () => 2 + 2

以上的代码都是在脚本顶层作用域下的合法的表达式。特别是第一个,r被称作标签,一个代码块的标签,可以被break打断,但是并不意味着你在顶层作用域下有一个参数r,它是无法被访问的:

function test(printTwo) {
  printing: {
    console.log('one');
    if (!printTwo) break printing;
    console.log('two');
  }
  console.log('three');
}

> test(false)
one
three

> test(true)
one
two
three

lab: function a () {}
console.log(lab) //ReferenceError: lab is not defined

花括号允许你将任意的表达式和语句包裹在一起,JS会计算每一个语句或者表达式,并且返回最后一个表达式的值:

{var a = 1; func(); 2 + 2} // 4

console.log(a) // 1

{var a = 1; func(); 2 + 2; var b = 2; 100; foo()} // 100

这种形式叫做块语句,和字面量对象有些相似,但是是不同的东西:

console.log({a: 1}) // { a: 1 }

console.log({var a = 1; 2 + 2}) // SyntaxError

字面量对象是表达式,可以当做一个值使用,而块语句是一个语句,虽然它有返回值,但是我们却不能使用。

有些场景下,这两者的形式可能会让人迷惑:

{
  foo: bar(1, 3)
}

{}

上面这两个,可以是字面量表达式,也可以是块语句,至于到底是什么,取决于它们出现的位置:

[] + {}  // "[object Object]"

{} + []  // 0

第一种情况,{}被当做表达式使用,[] + {}使对象转化为字符串;第二种情况{}是一个块语句,那最后返回值其实就是+[]的值,确实是0。

还有一些更加过分的例子,但是只要记住:块语句虽然有返回值,却不可以被当做表达式使用

{1} + 1  // 1 而不是2

{1 + 1} + 2  // 2 而不是4

参考链接