第 3 章 语言基础



前言

虽然之前在课堂上已经学过 JavaScript 的相关用法了,但是现在要开始找工作的时候才发现好多知识点都忘记了(正所谓基础不牢,地动山摇😂),而且有些知识点学校教材上都没有,便有了要写这个 JavaScript 学习系列文章的想法。

由于博主只是做知识点总结与学习笔记分享,导致本系列文章对于 JavaScript 的使用方面介绍较少,还望读者能够自己多勤学苦练。废话不多说,让我们开始第 3 章 语言基础 的学习吧(本文内容主要基于 ECMAScript 第 6 版)。


一、语法

ECMAScript 的语法很大程度上借鉴了 C 语言和其他类 C 语言,如 Java 和 Perl。熟悉这些语言的开发者,应该很容易理解 ECMAScript 宽松的语法。

1.区分大小写

ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。

2.标识符

所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:

  • 第一个字符必须是一个字母下划线(_)或美元符号($);
  • 剩下的其他字符可以字母下划线美元符号数字

另外,推荐标识符使用小驼峰式命名法(lower camel case),即第一个单词以小写字母开始,第二个单词的首字母大写。例如:firstName、doSomethingImportant。因为这种形式跟 ECMAScript 内置函数和对象的命名方式一致。

注意:关键字、保留字、true、false 和 null 不能作为标识符。

3.注释

ECMAScript 采用 C 语言风格的注释,包括单行注释和块注释。单行注释以两个斜杠字符开头,如:

// 单行注释

块注释以一个斜杠和一个星号(/)开头,以它们的反向组合(/)结尾,如:

/* 这是多行
注释 */ 

4.严格模式

ECMAScript 5 增加了严格模式(strict mode)的概念。严格模式是一种不同的 JavaScript 解析和执行模型,ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。要对整个脚本启用严格模式,在脚本开头加上这一行:

"use strict";

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

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

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

5.语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子所示:

let sum = a + b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐

注意:虽然现在有的格式化插件在每条语句后省略了分号,但是建议初学者还是养成加分号的习惯

多条语句可以合并到一个 C 语言风格的代码块中。代码块由一个左花括号({)标识开始,一个右花括号(})标识结束:

// 有效,但容易导致错误,应该避免
if (test) 
  console.log(test); 

// 推荐
if (test) { 
  console.log(test); 
} 

注意:虽然 if 之类的控制语句在执行单条语句时可以不使用代码块,但是并不推荐这样

二、关键字与保留字

ECMA-262 第 6 版规定的所有关键字(共33个)如下:

breakdointypeof
caseelseinstanceofvar
catchexportnewvoid
classextendsreturnwhile
constfinallysuperwith
continueforswitchyield
debuggerfunctionthis
defaultifthrow
deleteimporttry

规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。以下是 ECMA-262 第 6 版为将来保留的所有词汇:

始终保留:
enum

严格模式下保留:
implements 、package 、public
interface 、protected 、static
let 、private

模块代码中保留:
await

注意:这些词汇不能用作标识符,但现在还可以用作对象的属性名。(不推荐)

三、变量

ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:varconstlet
注意:var 在 ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

1.var 关键字

要定义变量,可以使用 var 操作符(注意 var 是一个关键字),后跟变量名(即标识符,如前所述):

var message;	// 定义了一个名为 message 的变量,可以用它保存任何类型的值(不初始化值为 undefined)

var message = "hi";	// message 被定义为一个保存字符串值 hi 的变量(但值和类型都可以改变,如下)

var message = "hi"; 
message = 100; // 合法,但不推荐
  1. var 声明作用域
    如果使用 var 操作符在函数内定义一个变量,该变量将在函数退出时被销毁,所以在函数外无法获取到。不过,在函数内定义变量时省略 var 操作符,可以创建一个全局变量。代码如下(示例):

    function test() { 
     var message = "hi"; // 局部变量
     tips = "ok"; // 全局变量
    } 
    test(); 
    console.log(message); // 出错
    console.log(tips); // "ok"
    

    注意:虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError。

  2. var 声明提升
    所谓“提升”就是可以在使用语句可以放在定义语句之前,且不会报错。代码如下(示例):

    function foo() { 
      console.log(age); 
      var age = 26;
    } 
    foo(); // undefined
    

    之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:

    function foo() { 
      var age; 
      console.log(age); 
      age = 26; 
    } 
    foo(); // undefined 
    

    此外,反复多次使用 var 声明同一个变量也没有问题:

    function foo() { 
      var age = 16; 
      var age = 26; 
      var age = 36; 
      console.log(age); 
    } 
    foo(); // 36
    

2.let 声明

let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域

if (true) { 
  var name = 'Matt'; 
  console.log(name); // Matt 
} 
console.log(name); // Matt
if (true) { 
  let age = 26; 
  console.log(age); // 26 
} 
console.log(age); // ReferenceError: age 没有定义

let 也不允许同一个块作用域中出现冗余声明。这样会导致报错:

var name; 
var name; 

let age; 
let age; // SyntaxError;标识符 age 已经声明过了
  1. 暂时性死区
    在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。

    // name 会被提升
    console.log(name); // undefined 
    var name = 'Matt'; 
    
    // age 不会被提升
    console.log(age); // ReferenceError:age 没有定义
    let age = 26; 
    
  2. 全局声明
    与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。

    var name = 'Matt'; 
    console.log(window.name); // 'Matt' 
    
    let age = 26; 
    console.log(window.age); // undefined
    
  3. 条件声明
    在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。

    <script> 
      var name = 'Nicholas'; 
      let age = 26; 
    </script> 
    
    <script> 
      // 假设脚本不确定页面中是否已经声明了同名变量
      // 那它可以假设还没有声明过
     
      var name = 'Matt'; 
      // 这里没问题,因为可以被作为一个提升声明来处理
      // 不需要检查之前是否声明过同名变量
     
      let age = 36; 
      // 如果 age 之前声明过,这里会报错
    </script>
    

    使用 try/catch 语句或 typeof 操作符也不能解决,因为条件块中 let 声明的作用域仅限于该块。

    注意:不能使用 let 进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。

  4. for 循环中的 let 声明
    在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部:

    for (var i = 0; i < 5; ++i) { 
      // 循环逻辑 
    } 
    console.log(i); // 5
    

    改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:

    for (let i = 0; i < 5; ++i) { 
      // 循环逻辑
    } 
    console.log(i); // ReferenceError: i 没有定义
    

    在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:

    for (var i = 0; i < 5; ++i) { 
      setTimeout(() => console.log(i), 0) 
    } 
    // 你可能以为会输出 0、1、2、3、4 
    // 实际上会输出 5、5、5、5、5 
    

    上面代码如果使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个 setTimeout 引用的都是不同的变量实例,这样 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

    这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-in 和 for-of 循环。

3.const 声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

const age = 26; 
age = 36; // TypeError: 给常量赋值

// const 也不允许重复声明
const name = 'Matt'; 
const name = 'Nicholas'; // SyntaxError 

// const 声明的作用域也是块
const name = 'Matt'; 
if (true) { 
  const name = 'Nicholas'; 
} 
console.log(name); // Matt

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制。

const person = {}; 
person.name = 'Matt'; // ok

4.声明风格及最佳实践

  1. 不使用 var
    有了 let 和 const,大多数开发者会发现自己不再需要 var 了。限制自己只使用 let 和 const有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。
  2. const 优先,let 次之
    使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。

四、数据类型

ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object(对象)。Object 是一种无序名值对的集合。因为在 ECMAScript 中不能定义自己的数据类型,所有值都可以用上述 7 种数据类型之一来表示。只有 7 种数据类型似乎不足以表示全部数据。但 ECMAScript 的数据类型很灵活,一种数据类型可以当作多种数据类型来使用。

1.typeof 操作符

因为 ECMAScript 的类型系统是松散的,所以需要一种手段来确定任意变量的数据类型。typeof 操作符就是为此而生的。对一个值使用 typeof 操作符会返回下列字符串之一:

  • "undefined"表示值未定义;
  • "boolean"表示值为布尔值;
  • "string"表示值为字符串;
  • "number"表示值为数值;
  • "object"表示值为对象(而不是函数)或 null;
  • "function"表示值为函数;
  • "symbol"表示值为符号。

注意 typeof 在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用typeof null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。

注意:严格来讲,函数在 ECMAScript 中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过 typeof 操作符来区分函数和其他对象。

2.Undefined 类型

Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值:

let message; 
console.log(message == undefined); // true 

注意:一般来说,永远不用显式地给某个变量设置 undefined 值。字面值 undefined 主要用于比较,而且在 ECMA-262 第 3 版之前是不存在的。增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。

对未声明的变量,只能执行一个有用的操作,就是对它调用 typeof。(对未声明的变量调用 delete 也不会报错,但这个操作没什么用,实际上在严格模式下会抛出错误。)

注意:即使未初始化的变量会被自动赋予 undefined 值,但我们仍然建议在声明变量的同时进行初始化。这样,当 typeof 返回"undefined"时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。

3.Null 类型

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因:

let car = null; 
console.log(typeof car); // "object"

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用,比如:

if (car != null) { 
  // car 是一个对象的引用
} 

undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下面的例子所示:

console.log(null == undefined); // true

用等于操作符(==)比较 null 和 undefined 始终返回 true。但要注意,这个操作符会为了比较而转换它的操作数。

4.Boolean 类型

Boolean(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值:true 和 false。这两个布尔值不同于数值,因此 true 不等于 1,false 不等于 0。下面是给变量赋布尔值的例子:

let found = true; 
let lost = false;

虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()转型函数:

let message = "Hello world!"; 
let messageAsBoolean = Boolean(message);

下表总结了不同类型与布尔值之间的转换规则。

数据类型转换为 true 的值转换为 false 的值
Booleantruefalse
String非空字符串“”(空字符串)
Number非零数值(包括无穷值)0、NaN
Object任意对象null
UndefinedN/A(不存在)undefined

5.Number 类型

ECMAScript 中最有意思的数据类型或许就是 Number 了。Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。

注意:由于 JavaScript 保存数值的方式,实际中可能存在正零(+0)和负零(-0)。正零和负零在所有情况下都被认为是等同的,这里特地说明一下。

  1. 浮点值
    要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但推荐加上。下面是几个例子:

    let floatNum1 = 1.1; 
    let floatNum2 = 0.1; 
    let floatNum3 = .1; // 有效,但不推荐
    

    因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。

    let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理
    let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
    

    ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:

    let floatNum = 3.125e7; // 等于 31250000
    

    注意:因为存在舍入错误,所以哪怕浮点数的精确度最高可达17位小数,但在算数计算中远不如整数精确。这种错误在其他使用相同格式的语言中也有。

  2. 值的范围
    由于内存的限制,ECMAScript 并不支持表示这个世界上的所有数值。ECMAScript 可以表示的最小数值保存 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e-324;可以表示的最大数值保存在 Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。

    如果计算返回正 Infinity 或负 Infinity,则该值将不能再进一步用于任何计算。这是因为 Infinity 没有可用于计算的数值表示形式。要确定一个值是不是有限大(即介于 JavaScript 能表示的最小值和最大值之间),可以使用 isFinite()函数,如下所示:

    let result = Number.MAX_VALUE + Number.MAX_VALUE; 
    console.log(isFinite(result)); // false
    

    注意:使用 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以获取正、负 Infinity。没错,这两个属性包含的值分别就是-Infinity 和 Infinity。

  3. NaN
    有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在 ECMAScript 中,0、+0 或-0 相除会返回 NaN:

    console.log(0/0); // NaN 
    console.log(-0/+0); // NaN 
    

    如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或-Infinity:

    console.log(5/0); // Infinity 
    console.log(5/-0); // -Infinity
    

    NaN 有几个独特的属性。首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算时这可能是个问题。其次,NaN 不等于包括 NaN 在内的任何值。例如,下面这个经典选项:

    console.log(NaN == NaN); // false
    

    为此,ECMAScript 提供了 isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。举例如下:

    console.log(isNaN(NaN)); // true 
    console.log(isNaN(10)); // false,10 是数值
    console.log(isNaN("10")); // false,可以转换为数值 10 
    console.log(isNaN("blue")); // true,不可以转换为数值
    console.log(isNaN(true)); // false,可以转换为数值 1
    

    注意:虽然不常见,但 isNaN() 可以用于测试对象。此时,首先会调用对象的 valueOf()方法,然后再确定返回的值是否可以转换为数值。如果不能,再调用 toString()方法,并测试其返回值。这通常是 ECMAScript 内置函数和操作符的工作方式。

  4. 数值转换
    有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个函数执行的操作也不同。

    Number()函数基于如下规则执行转换。

    • 布尔值,true 转换为 1,false 转换为 0。
    • 数值,直接返回。
    • null,返回 0。
    • undefined,返回 NaN。
    • 字符串,应用以下规则。
      • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number(“1”)返回 1,Number(“123”)返回 123,Number(“011”)返回 11(忽略前面的零)。
      • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
      • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整
        数值。
      • 如果是空字符串(不包含字符),则返回 0。
      • 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
    • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()方法,再按照转换字符串的规则转换。

parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。不同的数值格式很容易混淆,因此 parseInt() 也接收第二个参数,用于指定底数(进制数)。

6.String 类型

String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号(")、单引号(')或反引号(`)标示,因此下面的代码都是合法的:

let firstName = "John"; 
let lastName = 'Jacob'; 
let lastName = `Jingleheimerschmidt`
  1. 字符字面量
    字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

    字面量含义
    \n换行
    \t制表
    \b退格
    \r回车
    \f换页
    \\反斜杠(\)
    \'单引号('),在字符串以单引号标示时使用
    \"双引号("),在字符串以双引号标示时使用
    \ `(中间没有空格)反引号(`),在字符串以反引号标示时使用
    \xnn以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如\x41 等于"A"
    \unnn以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如\u03a3 等于希腊字符"Σ"

注意:如果字符串中包含双字节字符,那么length 属性返回的值可能不是准确的字符数。

  1. 字符串的特点
    ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:

    let lang = "Java"; 
    lang = lang + "Script";
    
  2. 转换为字符串
    有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的 toString()方法。这个方法唯一的用途就是返回当前值的字符串等价物。比如:

    let age = 11; 
    let ageAsString = age.toString(); // 字符串"11" 
    let found = true; 
    let foundAsString = found.toString(); // 字符串"true"
    

    但 null 和 undefined 值没有 toString()方法。如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。

    • 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
    • 如果值是 null,返回"null"。
    • 如果值是 undefined,返回"undefined"。

    注意:用加号操作符给一个值加上一个空字符串""也可以将其转换为字符串

  3. 模板字面量
    ECMAScript 6 新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:

    let myMultiLineString = 'first line\nsecond line'; 
    let myMultiLineTemplateLiteral = `first line 
    second line`; 
    
    console.log(myMultiLineString); 
    // first line 
    // second line" 
    
    console.log(myMultiLineTemplateLiteral); 
    // first line
    // second line
     
    console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
    

    注意:模板字面量会保持反引号内部的空格,可能造成缩进不当

  4. 字符串插值
    字符串插值通过在${}中使用一个 JavaScript 表达式实现:

    let value = 5; 
    let exponent = 'second'; 
    // 以前,字符串插值是这样实现的:
    let interpolatedString = 
     value + ' to the ' + exponent + ' power is ' + (value * value); 
     
    // 现在,可以用模板字面量这样实现:
    let interpolatedTemplateLiteral = 
     `${ value } to the ${ exponent } power is ${ value * value }`; 
     
    console.log(interpolatedString); // 5 to the second power is 25 
    console.log(interpolatedTemplateLiteral); // 5 to the second power is 25 
    

    所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript 表达式都可以用于插值。

  5. 模板字面量标签函数
    模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。

    let a = 6; 
    let b = 9; 
    function simpleTag(strings, aValExpression, bValExpression, sumExpression) { 
     console.log(strings); 
     console.log(aValExpression); 
     console.log(bValExpression); 
     console.log(sumExpression); 
     return 'foobar'; 
    } 
    let untaggedResult = `${ a } + ${ b } = ${ a + b }`; 
    let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`; 
    // ["", " + ", " = ", ""] 
    // 6 
    // 9 
    // 15 
    console.log(untaggedResult); // "6 + 9 = 15" 
    console.log(taggedResult); // "foobar" 
    
  6. 原始字符串
    使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。为此,可以使用默认的 String.raw 标签函数:

    // Unicode 示例
    // \u00A9 是版权符号
    console.log(`\u00A9`); // © 
    console.log(String.raw`\u00A9`); // \u00A9 
    
    // 换行符示例
    console.log(`first line\nsecond line`); 
    // first line 
    // second line 
    
    console.log(String.raw`first line\nsecond line`); // "first line\nsecond line" 
    
    // 对实际的换行符来说是不行的
    // 它们不会被转换成转义序列的形式
    console.log(`first line
    second line`); 
    // first line 
    // second line 
    
    console.log(String.raw`first line 
    second line`); 
    // first line 
    // second line 
    

    另外,也可以通过标签函数的第一个参数,即字符串数组的.raw 属性取得每个字符串的原始内容。

7.Symbol 类型

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

  1. 符号的基本用法
    符号需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回 symbol。

    let sym = Symbol(); 
    console.log(typeof sym); // symbol
    

    最重要的是,Symbol() 函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象。如果你确实想使用符号包装对象,可以借用 Object()函数。

  2. 使用全局符号注册表
    如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册
    表中创建并重用符号。

    为此,需要使用 Symbol.for()方法:

    let fooGlobalSymbol = Symbol.for('foo'); 
    console.log(typeof fooGlobalSymbol); // symbol
    

    还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

    // 创建全局符号
    let s = Symbol.for('foo'); 
    console.log(Symbol.keyFor(s)); // foo 
    
    // 创建普通符号
    let s2 = Symbol('bar'); 
    console.log(Symbol.keyFor(s2)); // undefined 
    
  3. 使用符号作为属性
    凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

  4. 常用内置符号
    ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。

    注意:在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。

  5. Symbol.asyncIterator
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator。由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。

  6. Symbol.hasInstance
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型。

  7. Symbol.isConcatSpreadable
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素”。ES6 中的 Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcat-Spreadable 的值可以修改这个行为。

  8. Symbol.iterator
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。

  9. Symbol.match
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”。String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

  10. Symbol.replace
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”。String.prototype.replace()方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

  11. Symbol.search
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”。String.prototype.search() 方法会使用以 Symbol.search 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

  12. Symbol.species
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义。

  13. Symbol.split
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用”。String.prototype. split()方法会使用以 Symbol.split 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

  14. Symbol.toPrimitive
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性上定义一个函数可以改变默认行为。

  15. Symbol.toStringTag
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。

  16. Symbol.unscopables
    根据 ECMAScript 规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”。设置这个符号并让其映射对应属性的键值为 true,就可以阻止该属性出现在 with 环境绑定中。

8.Object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:

let o = new Object(); 

每个 Object 实例都有如下属性和方法。

  • constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object() 函数。
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty(“name”))或符号。
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。

因为在 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法。

五、操作符

ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf()和/或 toString()方法来取得可以计算的值。

1.一元操作符

只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是 ECMAScript中最简单的操作符。

  1. 递增(++)/递减(--)操作符
  2. 一元加(+)和减(-

2.位操作符

接下来要介绍的操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为32 位整数,再进行位操作,之后再把结果转换为 64 位。

  1. 按位非(~
  2. 按位与(&
  3. 按位或(|
  4. 按位异或(^
  5. 左移(<<
  6. 有符号右移(>>
  7. 无符号右移(>>>

3.布尔操作符

布尔操作符一共有 3 个:逻辑非(!)、逻辑与(&&)和逻辑或(||)。

4.乘性操作符

ECMAScript 定义了 3 个乘性操作符:乘法(*)、除法(/)和取模(%)。

5.指数操作符

ECMAScript 7 新增了指数操作符,Math.pow()现在有了自己的操作符**,结果是一样的。

6.加性操作符

加性操作符,即加法(+)和减法(-)操作符,一般都是编程语言中最简单的操作符。不过,在 ECMAScript 中,这两个操作符拥有一些特殊的行为。与乘性操作符类似,加性操作符在后台会发生不同数据类型的转换。

7.关系操作符

关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。这几个操作符都返回布尔值。

8.相等操作符

  1. 等于和不等于
    ECMAScript 中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回 true。不等于操作符用叹号和等于号(!=)表示,如果两个操作数不相等,则会返回 true。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。

  2. 全等和不全等
    全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操作符由 3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true,比如:

    let result1 = ("55" == 55); // true,转换后相等
    let result2 = ("55" === 55); // false,不相等,因为数据类型不同
    

9.条件操作符

条件操作符是 ECMAScript 中用途最为广泛的操作符之一,语法跟 Java 中一样:

variable = boolean_expression ? true_value : false_value;

10.赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量,如下所示:

let num = 10; 

复合赋值使用乘性、加性或位操作符后跟等于号(=)表示。每个数学操作符以及其他一些操作符都有对应的复合赋值操作符,但是使用它们不会提升性能。

11.逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如下所示:

let num1 = 1, num2 = 2, num3 = 3; 

六、语句

1.if 语句

2.do-while 语句

3.while 语句

4.for 语句

5.for-in 语句

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

for (property in expression) statement 

6.for-of 语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

for (property of expression) statement

7.标签语句

标签语句用于给语句加标签,语法如下:

label: statement

标签语句的典型应用场景是嵌套循环。

8.break 和 continue 语句

9.with 语句

with 语句的用途是将代码作用域设置为特定的对象,其语法是:

with (expression) statement;

使用 with 语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利。

严格模式不允许使用 with 语句,否则会抛出错误。

警告:由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with 语句。

10.switch 语句

七、函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。

以下是函数的基本语法:

function functionName(arg0, arg1,...,argN) { 
 statements 
} 

ECMAScript 中的函数与其他语言中的函数不一样。

  • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  • 不指定返回值的函数实际上会返回特殊值 undefined。

注意:最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。

严格模式对函数也有一些限制:

  • 函数不能以 eval 或 arguments 作为名称;
  • 函数的参数不能叫 eval 或 arguments;
  • 两个命名参数不能拥有同一个名称。

如果违反上述规则,则会导致语法错误,代码也不会执行。


总结

JavaScript 的核心语言特性在 ECMA-262 中以伪语言 ECMAScript 的形式来定义。ECMAScript 包含所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的机制。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。

以上就是本篇文章的全部内容了,下一篇文章我们将学习“第 4 章 变量、作用域与内存”,如果想要第一时间获取相关文章,欢迎关注博主,并订阅专栏。您的支持与鼓励将成为我不断前行地动力!

最后,如果本篇文章对正在阅读的您有所帮助或启发,请不要吝啬您的点赞收藏评论及分享,这样就有可能帮助到更多的人了。同时也欢迎留下您遇到的问题,让我们一起探讨学习,共同进步!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值