JavaScript 基础(二):语法、变量声明(var、let、const)、关键字

565 阅读11分钟

一、语法

1、基础语法

  • 区分大小写

    • 无论是变量、函数名还是操作符,都区分大小写。换句话说,变量 test 和变量 Test 是两个不同的变量。
  • 标识符(变量) 命名

    • 字母、下划线(_)或美元符号($) 组成,首字母必须非数字
    • 标识符中的字母可以是扩展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符,如 À 和 Æ(但不推荐使用)。
    • 推荐驼峰命名,如:userInfo。因为这种形式跟 ECMAScript 内置函数和对象的命名方式一致。

    注意:关键字、保留字、truefalsenull 不能作为标识符。

  • 注释

    • 单行注释: // ...
    • 多行注释/* ... */

    使用热键!

    在大多数的编辑器中,一行代码可以使用 Ctrl+/ 热键进行单行注释,诸如 Ctrl+Shift+/ 的热键可以进行多行注释(选择代码,然后按下热键)。

    对于 Mac 电脑,应使用 Cmd 而不是 Ctrl,使用 Option 而不是 Shift

  • 语句

    • 分号; 分隔。通常,换行符也被视为分隔符,所以在换行情况下也可不使用分号。
      • 加分号有助于:
        • 防止省略造成的问题,比如可以避免输入内容不完整。
        • 便于开发者通过删除空行来压缩代码。
        • 有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误
    • 多行语句可以用代码块{}包起来。代码块后不需要加分号,但是加了也不会报错,会被自动忽略。
      • 可以让内容更清晰,在需要修改代码时也可以减少出错的可能性。

一个不加分号导致的错误的例子:

// 1
[1, 2].forEach(alert) // 先显示 1,然后显示 2

// 2
alert("There will be an error")
[1, 2].forEach(alert)
// 只有第一个 alert 语句的内容被显示了出来,随后我们收到了一个错误!
// 因为 JavaScript 并不会在方括号 [...] 前添加一个隐式的分号
// alert("There will be an error")[1, 2].forEach(alert)

// 3
alert("All fine now");
[1, 2].forEach(alert)
// 先显示 There will be an error,再是 1,然后是 2

2、严格模式 'use strict'

长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题。新的特性被加入,旧的功能也没有改变。

这么做有利于兼容旧代码,但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中。

这种情况一直持续到 2009 年 ECMAScript 5 (ES5) 的出现。ES5 规范增加了新的语言特性并且修改了一些已经存在的特性。为了保证旧的功能能够使用,大部分的修改是默认不生效的。你需要一个特殊的预处理指令 —— 'use strict' 来明确地激活这些特性。ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误

注意:'use strict' 必须在脚本的顶部或函数体的开头才能生效

// 1.
alert('some code');
// 下面的 "use strict" 会被忽略,必须在最顶部。

'use strict';

// 严格模式没有被激活

// 2.
'use strict';
alert('some code');
// 严格模式激活

可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:

function doSomething() { 
    'use strict'; 
    // 函数体 
} 

所有现代浏览器都支持严格模式。

没有办法取消 use strict

没有类似于 "no use strict" 这样的指令可以使程序返回默认模式。一旦进入了严格模式,就没有回头路了。

语言的一些现代特征(比如classmodule)会自动启用 use strict

2.1 浏览器控制台启动 use strict

当你使用 开发者控制台 运行代码时,请注意它默认是不启动 use strict

有时,当 use strict 会对代码产生一些影响时,你会得到错误的结果。

那么,怎么在控制台中启用 use strict 呢?

首先,你可以尝试搭配使用 Shift+Enter 按键去输入多行代码,然后将 use strict 放在代码最顶部,就像这样:

'use strict'; <Shift+Enter 换行>
//  ...你的代码
<按下 Enter 以运行>

在大部分浏览器中都有效,像 Firefox 和 Chrome。

如果依然不行,例如你使用的是旧版本的浏览器,那么有一种很丑但可靠的启用 use strict 的方法。将你的代码放在这样的包装器中:

(function() {
  'use strict';

  // ...你的代码...
})()

二、变量声明

1、letvarconst

  • var

    • 可以重复声明

    • 作用域:全局作用域、函数作用域

      注意:没有块级作用域。因为在早期的 js 中,块没有词法环境。

      • 使用 var 声明变量时,变量会被自动添加到最接近的上下文

      • 在函数内定义变量时省略 var 操作符,会自动被添加到全局上下文,可在函数外访问(不推荐,严格模式下抛出 ReferenceError 错误);

      • 非函数内定义的变量会被添加到全局上下文。浏览器的全局上下文对象是 window 对象,可通过 window 对象访问变量(对 Node.js 而言,它的名字是 “global”,其它环境可能用的是别的名字。它有一个通用名称 globalThis,所有环境都应该支持该名称)。

      • 在浏览器中,除非我们使用 modules,否则使用 var 声明的全局函数和变量会成为全局对象的属性。

    • 会进行变量提升(即声明前置:声明前,调用不报错)。

    var 声明会被拿到函数或全局作用域的顶部 ,位于作用域中所有代码之前。这个现象叫作“提升”(hoisting)。提升让同一作用域中的代码不必考虑变量是否声明就可直接使用(输出 undefined 而不是 Reference Error)。

    注意:声明会被提升,但是赋值不会。 声明在函数刚开始执行的时候(“提升”)就被处理了,但是赋值操作始终是在它出现的地方才起作用

  • let

    • 同一作用域(块)下 不能重复声明

      • 对声明冗余报错不会因混用 letvar 而受影响
    • 作用域:全局作用域、块级作用域(由最近的一对包含花括号 {} 界定);

    块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let

    • 不会提升先声明,再使用)。

      • let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。

      严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var 是不一样的。

  • const

    • 常量只能在声明时赋值,并且一旦赋值不能修改

      • const 对象外层不可改变,但内层可以

      • 要内层也不能改变,可以使用 Object.freeze(obj) 冻结对象。这样再给属性赋值时虽然不会报错,但会静默失败

    • 不能重复声明

    • 作用域:块级作用域

    • 不会提升

在严格模式下,不能定义名为 evalarguments 的变量,否则会导致语法错误。

1.1 var

// 1. var 可以重复声明
var a = 1;
var a = 12; // 这个 var 无效,因为变量已经被声明过了,如果有值则进行覆盖。
console.log(a) // 12
var a; // 这个 var 无效,因为变量已经被声明过了,无值不进行覆盖。
console.log(a) // 12

// 2. 作用域
// 变量未经声明就被初始化,添加到全局上下文
function add(num1, num2) { 
 sum = num1 + num2; // 变量未经声明就被初始化,会自动被添加到全局上下文
 return sum; 
} 
let result = add(10, 20); // 30 
console.log(sum); // 30 
// sum被添加到全局上下文,在函数退出之后依然存在,可以在函数外被访问到

// 3. 浏览器全局对象 window 的属性
var name = 'Matt'; 
console.log(window.name); // 'Matt' 其实是被提升到全局上下文,浏览器的全局上下文是 window 对象

// 4. 变量提升
function sayHi() { 
    phrase = "Hello";
    if (false) { 
        var phrase; 
    }
    alert(phrase); 
} 
sayHi();
// ……从技术上讲,它与下面这种情况是一样的(`var phrase` 被上移至函数开头):
function sayHi() { 
    var phrase;
    phrase = "Hello";
    if (false) { 
         
    }
    alert(phrase); 
} 
sayHi();

// 注意:声明会被提升,但是赋值不会
function sayHi() { 
    console.log(phrase); 
    var phrase = "Hello";
} 
sayHi(); // undefined
// 等同于
function sayHi() { 
    var phrase;
    console.log(phrase); 
    phrase = "Hello";
} 
sayHi(); // undefined

1.2 let

// 1. let 不可重复声明
let a = 1
let a = 2 // 报错

// 1.1 对声明冗余报错不会因混用 let 和 var 而受影响
let a = 1
var a = 2 // 报错

// 2. 块作用域
// 这不是对象字面量,而是一个独立的块
// JavaScript 解释器会根据其中内容识别出它来
{ 
 let a; 
} 
console.log(a); // ReferenceError: a 没有定义

// 3. let 不会提升
console.log(e) // ReferenceError: e is not defined。
let e = 1

1.3 const

// 1.1 常量声明时必须赋值
const b; // SyntaxError: Missing initializer in const declaration。

// 1.2 常量一旦赋值不能修改
const d = 20;
d = 30; // TypeError: Assignment to constant variable.

// 1.3 const 对象冻结 
// const 声明仅固定了对象的值,而不是值(该对象)里面的内容。
// 仅当我们尝试将 `obj=...` 作为一个整体进行赋值时,const 会抛出错误。
const obj = {name: '张三', age: 20}
// obj = { name: '李四' } // TypeError: Assignment to constant variable.

// const 对象外层不可改变,但内层可以
obj.name = '李四' // 不报错
console.log(obj) // {name: '李四', age: 20}

// 要内层也不能改变,可以使用 Object.freeze(obj) 冻结对象
Object.freeze(obj);
obj.name = '王五' // 不报错,但也不会改变
console.log(obj) // {name: '李四', age: 20}

// 2. 不能重复声明
const a = 1
const a = 2 // 报错

// 3. 不会提升
console.log(g) // ReferenceError: 无法在初始化之前访问'g'
const g = 20

2、letvar 的差异

  • var 可以重复声明,let 不可重复声明。

  • let 声明的范围是块作用域var 声明的范围是函数作用域

  • var 会进行变量提升let 不会被提升(暂时性死区)。

  • 全局作用域中声明 var挂在 window 对象上let 则不会。

  • let 不能依赖条件声明模式。因为 let 是块级作用域,条件块中 let 声明的作用域仅限于该块。

/* 1. 重复声明 */
// 1.1 var 可以重复声明
var a = 1;
var a = 2
console.log(a) // 2
// 1.2 let 不可重复声明
let a = 1
let a = 2 // 报错

/* 2. 作用域 */
if(true) {
    let b = 2; // 块级作用域
    // var b = 4; // 报错,let 同一作用域下不能重复声明。对声明冗余报错不会因混用 let 和 var 而受影响。
    var c = 3; // 函数作用域
    console.log(b); // 2
}
// console.log(b); // ReferenceError: b is not defined
console.log(c); // 3

/* 3. 变量提升,声明前置 */
// 3.1 var 会提升
console.log(d) // undefined
var d = 1
// 等价于
var d;
console.log(d);
d = 1;

// 3.2 let 不会提升
console.log(e) // ReferenceError: e is not defined。
let e = 1

/* 4. window 对象的属性 */
var name = 'Matt'; 
console.log(window.name); // 'Matt' 其实是被提升到全局上下文,浏览器的全局上下文是 window 对象
let age = 26; 
console.log(window.age); // undefined

/* 5. 条件声明 */
<script>
    let name = 'Nicholas'; 
    let age = 36; 
</script> 
<script> 
    // 假设脚本不确定页面中是否已经声明了同名变量,那它可以假设还没有声明过
    if (typeof name === 'undefined') { 
        let name; 
    } 
    name = 'Matt';  // name 被限制在 if {} 块的作用域内,因此这个赋值形同全局赋值

    try { 
        console.log(age);
    } 
    catch(error) { 
        let age; 
    } 
    age = 26; // age 被限制在 catch {}块的作用域内,因此这个赋值形同全局赋值
</script>

3、声明风格及最佳实践

  • 不使用 var

    • 有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值
  • const 优先,let 次之

    • 由于 const 声明暗示变量的值是单一类型且不可修改浏览器运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找,保持变量不变

    • 让静态代码分析工具提前发现不合法的赋值操作。能让开发者迅速发现因意外赋值导致的非预期行为。

  • let 手册地址:developer.mozilla.org/zh-CN/docs/…

  • const 手册地址:developer.mozilla.org/zh-CN/docs/…

4、面试题: varletconst 的区别

  • var重复声明声明时挂在 window 对象上函数作用域;会变量提升
  • let 不允许重复声明;不会通过 window 访问;块级作用域不会变量提升;声明前使用会造成“暂时性死区”;
  • const 不允许重复声明一旦声明就不允许更改(注意下对象,内层可以被更改); 不会通过 window 访问;块级作用域不会变量提升;

三、关键字和保留字

ECMA-262 描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作。按照规定,保留的关键字不能用作标识符或属性名

ECMA-262 第 6 版规定的所有关键字如下:

breakdointypeof
caseelseinstanceofvar
catchexportnewvoid
classextendsreturnwhile
constfinallysuperwith
continueforswitchyield
debuggerfunctionthis
defaultifthrow
deleteimporttry

规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定用途,但它们是保留给将来做关键字用的。

以下是 ECMA-262 第 6 版为将来保留的所有词汇:

始终保留:

标题
enum

严格模式下保留:

implementspackagepublic
interfaceprotectedstatic
letprivate

模块代码中保留:

标题
await

不要使用关键字和保留字作为标识符和属性名,以确保兼容过去和未来的 ECMAScript 版本