最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • (第三章)我一字一行地重读红宝书

    正文概述 掘金(大大?)   2021-03-03   420

    引言

    历经两个多月断断续续地读完了JavaScript的经典书籍红宝书 -《JavaScript高级程序设计(第4版)

    详细地读完一遍后发觉整本书知识点全而泛,乍一想每一章的细节,还是略显模糊。

    于是督促自己计划编写每一章的着重点再次加深印象和理解,顺便记录自己的所学所想所悟。方便自身利用电脑的快速搜索关键词来进行快速定位和学习,也希望能帮助到有需要的同学们哈。

    若是想要系统仔细的学习,当然还是看原书比较好,我也是强烈推荐的噢!这里内容只当个人复习和总结。

    提示: 一些个人主观认为不重要或不流行的章节将进行删减

    3. 语言基础

    ECMA-262 以一个名为 ECMAScript 伪语言的形式,定义了 JavaScript 的语法、操作符、数据类型和内置功能等。到2017年底,大多数主流浏览器几乎或全部实现了这一版的规范,为此,本章内容主要基于 ECMAScript 第6版。

    3.1 语法

    ECMAScript 的语法很大程度上借鉴了 C 语言和其他类 C 语言,如 Java 和 Perl。

    3.1.1 区分大小写

    在 ECMAScript 中一切都区分大小写,无论是变量、函数名还是操作符。类似地,typeof 不能作为函数名(因为是一个关键字),但 Typeof 是一个完全有效地函数名。

    3.1.2 标识符

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

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

    标识符中的字母可以是扩展 ASCII 中的字母,也可以是 Unicode 的字母字符(但不推荐使用)。

    ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写。如: firstSecond、myCar。

    3.1.3 注释

    ECMAScript 采用 C 语言风格的注释,包括单行注释和块注释。

    • // 单行注释
      
    • /* 这是多行
      注释 */
      

    3.1.4 严格模式

    ECMAScript 5 增加了严格模式的概念。ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。

    在脚本文件的开头加上这一行即可声明: "use strict"; (一个预处理指令)

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

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

    严格模式会影响 JavaScript 执行的很多方面且所有现代浏览器都支持严格模式。

    3.1.5 语句

    • 分号结尾

      • let sum = a + b	// 无分号有效,不推荐
        
      • let diff = a - b;      // 加分号有效,推荐
        
        • 让解析器确定语句在哪里结尾,有助于防止省略造成的问题。
        • 解析器会尝试在合适的位置补上分号以纠正语法错误,有助于在某些情况下提升性能。
    • 代码块: 由一个左花括号({)标识开始,一个右花括号(})标识结束。

      • if (test)	// 单条语句有效,但不推荐
          console.log(test);
        
      • if (test) {	// 推荐
          console.log(test);
        }
        
        • 在控制语句中使用代码块可以让内容更清晰,减少出错的可能性。

    3.2 关键字与保留字

    ECMA-262 描述了一组保留的关键字具有一定的特定用途,按照规定,保留的关键字不能用做识别符或属性名。

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

    未来的保留字,虽然在语言中没有特定用途,但它们是保留给将来作为关键字。

    • 始终保留

    • 严格模式下保留

    • 模块代码中保留

    3.3 变量

    ECMAScript 变量是松散类型,是指变量可以用于保存任何类型的数据。有 3 个关键字可以声明变量:var、const 和 let。其中 var 在ECMAScript 所有版本中均可使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

    3.3.1 var 关键字

    var message;	// 定义了名为 message 的变量,可以用它保存任何类型的值。(不初始化的情况下,变量会保存一个特殊值 undefined)
    
    var message = "hi";
    message = 100;	// 合法,但不推荐改变数据值的类型
    
    var messgae = "hi",
        found = false,
        age = 29;	// 如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量
    

    3.3.1.1 var 声明作用域

    使用 var 操作符定义的变量会成为包含它的函数的局部变量。

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

    3.3.1.2 var 声明提升

    var 关键字声明的变量会自动提升到函数作用域顶部:

    function foo() {
      console.log(age);
      var age = 26;
    }
    foo();	// undefined
    
    //	之所以不会出错,是因为等效于如下
    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
    

    3.3.2 let 声明

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

    // 函数作用域
    function foo() {
      if (true) {
        var name = 'Matt';
        console.log(name);	// Matt
      }
    	console.log(name);	// Matt
    }
    foo();
    
    // 块作用域
    function foo() {
      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 已经声明过了
    
    var name;
    let name;	// SyntacError
    

    3.3.2.1 暂时性死区

    let 声明的变量不会在作用域中被提升。在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出错误。

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

    3.3.2.2 全局声明

    var name = 'Matt';	// var 声明的变量会成为 window 对象的属性
    console.log(window.name);	// 'Matt'
    
    let age = 26;	// let 声明的变量不会
    console.log(window.age);	// undefined
    

    3.3.2.4 for 循环中的 let 声明

    像类似于 for、for-in 和 for-of 循环的块作用域,var 和 let 声明变量通常会导致结果大为不同。

    // var 是函数作用域,因此所有 i 一直为同一个变量
    for (var i = 0; i < 5; ++i) {
      setTimeout(() => console.log(i), 0)
    }	// 5、5、5、5、5
    
    // let 是块作用域,每次迭代循环都会声明一个新的迭代变量
    for (let i = 0; i < 5; ++i) {
      setTimeout(() => console.log(i), 0)
    }	// 0、1、2、3、4
    

    3.3.3 const 声明

    const 声明与 let 声明基本一致,区分其重要区别如下:

    // 必须同时初始化变量
    const age;	// SyntaxError
    
    // 不能重复赋值
    const age = 26;
    age = 36;	// TypeError
    

    const 声明的限制只适用于它指向的变量的引用,若 const 变量引用的是一个对象,那么是可以修改这个对象内部的属性。

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

    3.3.4 声明风格及最佳实践

    • 不使用 var
      • 有了 let 和 const,开发者应该限制自己只使用 let 和 const 有助于提升代码质量。
        • 有明确的作用域
        • 声明位置
        • 不变的值
    • const 优先,let 次之
      • 开发者应优先使用 const 来声明变量,只会提前知道未来会有修改时,再使用 let。
        • 让浏览器运行时强制保持变量不变
        • 让静态代码分析工具提前发现不合法的赋值操作
        • 更能迅速发现因意外赋值导致的非预期行为

    3.4 数据类型

    简单的数据类型(也称为原始类型)

    • ES6之前
      • Undefined、Null、Number、String、Boolean
    • ES6新增
      • Symbol(符号)
    • ES10新增
      • BigInt

    引用数据类型: Object(对象)是一种无序名值对的集合。

    3.4.1 typeof 操作符

    因为 ECMAScript 的类型系统是松散,使用 typeof 操作符返回下列字符串之一

    • "undefined" 表示值未定义
    • "boolean" 表示值为布尔值
    • "string" 表示值为字符串
    • "number" 表示值为数值
    • "object" 表示值为对象或 null
    • "function" 表示值为函数
    • "symbol" 表示值为符号
    console.log(typeof "abc");	// "string"
    console.log(typeof 123);	// "number"
    

    3.4.2 Undefined 类型

    Undefined 数据类型只有一个特殊值: undefined。

    当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋于了 undefined 值。

    let message;	// 这个变量被声明,被赋于了 undefined 值
    if (message) {
      // 这个块不会执行
    }
    if (!message) {
      // 这个块会执行
    }
    if (age) {	// 未声明变量
      // 这里会报错
    }
    

    3.4.3 Null 类型

    Null 数据类型只有一个特殊值: null。

    逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因。因此定义将来要保存对象值的变量时,建议使用 null 来初始化。

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

    3.4.4 Boolean 类型

    Boolean 数据类型有两个字面值: true 和 false(区分大写)。

    在所有其他 ECMAScript 类型的值都有相应布尔值的等价形式,调用特定的 Boolean() 转型函数或 !! 隐形转换。

    数据类型转换为 true 的值转换为 false 的值
    BooleantruefalseString非空字符串" "(空字符串)Number非零数值0、NaNObject任意对象nullUndefinedN/A(无)undefined

    3.4.5 Number 类型

    Number 类型使用 IEEE 754 格式表示整数和浮点数(在某些语言中也叫双精度值)。

    • 十进制整数:直接写出即可

      • let intNum = 55;
    • 八进制整数:第一个数字必须是零(0)+ 相应的八进制数字(0~7)。否则将当成十进制处理。

      • let octalNum1 = 070;	// 八进制 56
        let octalNum2 = 079;	// 无效八进制值,当成 79 处理
        
    • 十六进制整数:前缀 0x(区分大小写)+ 相应十六进制数字(0~9 以及 A~F)不分大小写。

      • let hexNum1 = 0xA;	// 十六进制 10
        let hexNum2 = 0x1f;	// 十六进制 31
        let hexNum3 = 0xg;	// 无效十六进制,报错
        

    3.4.5.1 浮点值

    定义浮点值,数值中必须包含小数点。

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

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

    • let floatNum1 = 1.;	// 整数 1 处理
      let floatNum2 = 10.0;	// 整数 10 处理
      

    3.4.5.2 值的范围

    • ECMAScript 可以表示的最小的数值:Number.MIN_VALUE 为 5e-324。

    • 最大的数值:Number.MAX_VALUE 为 1.797 693 134 862 315 7e+308。

    • 若某个计算超出了 JavaScript 可以表示的范围,将会自动转换为特殊的 Infinity 和 -Infinity,该值将不能再进一步用于任何计算。

    • 要确定一个值是不是有限大(介于 JavaScript 能表示的范围中),使用 isFinite() 函数

      • let InfinityNum = Number.MAX_VALUE + Number.MAX_VALUE;
        console.log(isFinite(InfinityNum));	// 如果参数是 NaN,正无穷大或者负无穷大,会返回 false,其他返回 true。
        

    3.4.5.3 NaN

    NaN:不是数值(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)如

    • 0、+0 或 -0 相除会返回 NaN

      • console.log(0/0);	// NaN
        console.log(-0/+0);	// NaN
        
    • 若分子非 0 值,分母为 0 或 -0 则返回无穷值

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

    NaN 不等于包括 NaN 在内的任何值

    console.log(NaN == NaN)	// false
    console.log(NaN === NaN)	// false
    
    // 可使用 isNaN() 函数接收任意类型的一个参数,将进行隐式转换返回 Boolean 值。
    console.log(isNaN(NaN));	// true
    console.log(isNaN(10));	// false,10是数值
    console.log(isNaN("10"));	// false,可以转换为数值 10
    console.log(isNaN(true));	// false,可以转换为数值 1
    console.log(isNaN("blue"));	// true,不可以转换为数值
    

    3.4.5.4 数值转换

    Number()、parseInt() 和 parseFloat(),可以将非数值转换为数值。

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

      • 布尔值,true 转换为 1,false 转换为 0。

      • 数值,直接返回。

      • null,返回 0.

      • undefined,返回 NaN。

      • 字符串,应用如下规则。

        • 字符串为数值字符(包含前缀有 +、- 情况),转换为一个十进制数值。

          • Number("1");	// 1
            Number("0123");	// 123
            
        • 字符串包含有效浮点值格式,则会转换为相应的浮点值。

          • Number("0.1");	// 0.1
            Number("00.123");	// 0.123
            
        • 字符串包含有效的十六进制格式数值,则会转换为与该进制对应的十进制整数数值。

          • Number("0xf");	// 15
            
        • 字符串为空字符串,则返回 0。

        • 字符串包含除上述情况之外的其他字符,则返回 NaN。

      • 对象,调用 valueof() 方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用 toString() 方法,再按照转换字符串规则转换。

    • parseInt() 函数在需要得到整数时可以优先使用。第一个参数接收一个字符串,第二个参数接收指定底数(进制数)。

      • 字符串前面的空格会被忽略,从第一个非空格字符开始转换。

      • 字符串第一个非空格字符不是数值字符、加号或减号,将立即返回 NaN。

      • 字符串第一个非空格字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。

      • 字符串为不同进制格式的数值时,也可以被该函数解释为对应进制格式整数。

      • let num1 = parseInt("   123");	// 123
        let num2 = parseInt("1234blue");	// 1234
        let num3 = parseInt("");	// NaN
        let num4 = parseInt("0xA");	// 10,解释为十六进制整数
        let num5 = parseInt("22.5");	// 22
        let num6 = parseInt("70");	// 70
        
      • 通过第二个参数,可以极大扩展转换后获得的结果类型

        • let num1 = parseInt("AF", 16);	// 175
          let num2 = parseInt("AF");	// NaN,未指定底数不能省略前缀。
          let num3 = parseInt("10", 2);	// 2, 按二进制解析。
          let num4 = parseInt("10", 8);	// 8, 按八进制解析。
          let num5 = parseInt("10", 10);	// 10, 默认按十进制解析。
          let num6 = parseInt("10", 16);	// 16, 按十六进制解析。
          
    • parseFloat() 函数与上述 parseInt() 函数类似。主要区别如下

      • 可以解析小数点,但仅有第一次出现的小数点有效。

      • 只解析十进制值,因此不能指定底数。

      • 若字符串数值为整数,JavaScript 为节省空间内存将自动转为整数。

      • let num1 = parseFloat("   1.23");	// 1.23
        let num2 = parseFloat("01.234blue");	// 1.234
        let num3 = parseFloat("");	// NaN
        let num4 = parseFloat("0xA");	// 0,无法解析进制数。
        let num5 = parseFloat("22.34.5");	// 22.34,第二个小数点当作普通字符处理。
        let num6 = parseFloat("3.125e7");	// 31250000
        

    3.4.6 String 类型

    String 数据类型表示零或多个 16 位 Unicode 字符序列,字符串可以如下三种形式表示:

    let str1 = "Lindada";
    let str2 = 'Lindada';
    let str3 = `Lindada`;
    

    ECMAScript 语法中表示三种形式的字符串对字符串的解释与某些语言不同,呈现的结果是相同的。

    3.4.6.1 字符字面量

    字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表示:

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

    3.4.6.2 字符串的特点

    ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,值便无法修改了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。

    let lang = "Java";
    lang += "Script";
    
    /**
     * 变量 lang 一开始为 "Java" 字符串,随后 lang 变量被修改定义为 "JavaScript" 字符串。
     * 这意味着修改的整个过程首先会先分配一个足够容纳 10 个字符的空间,填充上 "Java" 和 "Script",最后销毁原始的字符串 "Java" 和 "Script"。
     * 所有处理均在后台发生完成。
     */
    

    3.4.6.3 转换为字符串

    有两种方式把一个值转换为字符串,toString() 方法和 String 转型函数。

    • toString():返回当前值的字符串等价物。

      • let age = 11;
        let age2Str = age.toString();	// "11"
        let found = true;
        let found2Str = found.toString();	// "true"
        
        • 几乎所有数据类型的值均有 toString() 方法,但除了 null 和 undefined 值没有 toString() 方法。
      • 一般情况下该方法不接收任何参数,不过在对数值类型数据调用该方法时,可以传入底数(进制数)来进行转换。

        • let num = 10;
          num.toString();	// "10"
          num.toString(2);	// "1010"
          num.toString(8);	// "12"
          num.toString(10);	// "10",默认以 10 为底。
          num.toString(16);	// "a"
          
    • String():当不确定一个值是否具有 toString() 方法时(null 或 undefined),可以使用 String() 转型函数。

      • 函数遵循如下规则:

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

        • String(10);	// "10"
          String(true);	// "true"
          String(null);	// "null"
          String(undefined);	// "undefined"
          

    3.4.6.4 模版字面量

    ECMAScript 6 新增模版字面量定义字符串的能力,保留换行字符,可以跨行定义字符串:

    // 结果一致
    let multiLineStr = 'first line\nsecond line';
    let multiLineTemplateLiteral = `first line
    second line`;
    // frist line
    // second line
    multiLineStr === multiLineTemplateLiteral;	// true
    

    模版字面量在定义模版时特别有用,比如下面这个 HTML 模版:

    let pageHTML = `
    <div>
    	<a href="#">
    		<span>Jake</span>
    	</a>
    </div>
    

    由于模版字面量会保持反引号内部的空格,所以格式正确的模版字符串应不带有缩进。

    let multiLineTemplateLiteral = `first line
    																second line`;
    console.log(multiLineTemplateLiteral.length);	// 38
    

    3.4.6.5 字符串插值

    技术上讲,模版字面量不是字符串,而是一种特殊的 JavaScript 语法表达式。在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。

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

      • let value = 5;
        let str = 'second';
        // 以前,实现字符串插值。
        let interpolatedStr = value + ' to the ' + str + 'power is ' + (value * value);
        // 5 to the second power is 25
            
        // 现在,使用模版字面量
        let interpolatedTemplateLiteral = `${value} to the ${str} power is ${value * value}`;
        // 5 to the second power is 25
        
    • 所有插入的值都会使用 toString() 强制转型为字符串。

    • 在插值表达式中可以调用函数和方法:

      • function capitalize(word) {
          return `${word[0].toUpperCase()} ${word.slice(1)}`;
        }
        console.log(`${capitalize('hello')}, ${capitalize('world')}!`);	// Hello, World!
        

    3.4.6.6 模版字面量标签函数

    模版字面量支持定义标签函数,标签函数会接收被插值记号分隔后的模版和对每个表达式求值的结果:

    let a = 6;
    let b = 9;
    
    function simpleTag(strings, aVal, bVal, sumVal) {
      console.log(strings);
      console.log(aVal);
      console.log(bVal);
      console.log(sumVal);
      
      return 'value';
    }
    
    let untaggedRes = `${a} + ${b} = ${a + b}`;
    let taggedRes = simpleTag`${a} + ${b} = ${a + b}`;
    // ["", " + ", " = ", ""]
    // 6
    // 9
    // 15
    
    console.log(untaggedRes);	// "6 + 9 = 15"
    console.log(taggedRes);	// "value"
    

    若不确定标签函数中的参数,可以使用剩余操作符(...):

    let a = 6;
    let b = 9;
    
    function simpleTag(strings, ...expressions) {
    	console.log(strings[0])
    	return strings[0] +
    		expressions.map((v, i) => {
    			console.log(`${v}${strings[i + 1]}`)
    			return `${v}${strings[i + 1]}`})
    		.join('');
    }
    
    let untaggedRes = `${a} + ${b} = ${a + b}`;
    let taggedRes = simpleTag`${a} + ${b} = ${a + b}`;
    // ""
    // "6 + "
    // "9 = "
    // "15"
    
    console.log(untaggedRes);	// "6 + 9 = 15"
    console.log(taggedRes);	// "6 + 9 = 15"
    

    3.4.6.7 原始字符串

    使用模版字面量也可以直接获取原始的模版字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。为此可以使用默认的 String.raw 标签函数:

    // Unicode 示例
    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"
    

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

    function printRaw(strings) {
      console.log('Actual characters:');
      for (const string of strings) {
        console.log(string);
      }
      
      console.log('Escaped characters:');
      for (const rawString of strings.raw) {
        console.log(rawString);
      }
    }
    
    printRaw`\u00A9${'and'}\n`;
    // Actual characters:
    // ©
    // (换行符)
    // Escaped characters:
    // \u00A9
    // \n
    

    3.4.7 Symbol 类型

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

    3.4.7.1 符号的基本用法

    符号需要使用 Symbol() 函数初始化,可以传入一个字符串参数对符号的描述,可通过这个描述来调试代码,但描述与符号定义或标识完全无关

    // 定义 Symbol
    let sym = Symbol();
    console.log(typeof sym);	// symbol
    
    let sym1 = Symbol();
    let sym2 = Symbol();
    console.log(sym1 === sym2);	// false
    
    let sym3 = Symbol('foo');
    let sym4 = Symbol('foo');
    console.log(sym3 === sym4);	// false
    

    Symbol() 函数不能用作构造函数,与 new 关键字使用,为避免创建符号包装对象。

    let mySymbol = new Symbol();	// TypeError: Symbol is not a constructor
    
    // 若确实需要符号包装对象,可以借助 Object() 函数
    let mySymbol = Symbol();
    let myWrappedSymbol = Object(mySymbol);
    console.log(typeof myWrappedSymbol);	// "object"
    

    3.4.7.2 使用全局符号注册表

    • Symbol.for() 方法:运行时的不同部分需要共享和重用符号的实例,那么可以使用一个符号串作为键。

      • let globalSymbol = Symbol.for('foo');
        // 后续使用相同字符串的调用会检查注册表,但会对应该符号的实例
        let otherGlobalSymbol = Symbol.for('foo');
        // 即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不相同
        let localSymbol = Symbol('foo');
            
        console.log(typeof fooGlobalSymbol);	// symbol
        console.log(fooGlobalSymbol === otherFooGlobalSymbol);	// true
        console.log(fooGlobalSymbol === localSymbol);	// false
        
    • 全局注册表中的符号必须使用字符串键来创建,因此传入 Symbol.for() 的值都会被转换为字符串。

    • 还可以使用 Symbol.keyFor() 来查询全局注册表

      • // 创建全局符号
        let s = Symbol.for('foo');
        console.log(Symbol.keyFor(s));	// foo
            
        // 创建普通符号,不是全局符号则返回 undefined
        let s2 = Symbol('foo');
        console.log(Symbol.keyFor(s2));	// undefined
            
        // 传入的不是符号,则抛出错误 TypeError
        Symbol.keyFor(123);	// TypeError: 123 is not a symbol
        

    3.4.7.3 使用符号作为属性

    • 凡事可以使用字符串或数值作为属性的地方,都可以使用符号替代。包括对象字面量属性和 Object.defineProperty() / Object.definedProperties() 定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

      • let s1 = Symbol('foo'),
            s2 = Symbol('bar');
            
        let o = {
          [s1]: 'foo val'
        };
        let o[s1] = 'foo val';
        Object.defineProperty(o, s1, {value: 'foo val'});
        Object.defineProperties(o, {
          [s1]: {value: 'foo value'},
          [s2]: {value: 'bar value'}
        })
        
    • 返回对象实例的属性方法

      • let s1 = Symbol('foo'),
            s2 = Symbol('bar');
            
        let o = {
          [s1]: 'foo val',
          [s2]: 'bar val',
          baz: 'baz val',
          qux: 'qux val'
        };
            
        Object.getOwnPropertySymbols(o);	// [Symbol(foo), Symbol(bar)]
        Object.getOwnPropertyNames(o);	// ["bar", "qux"]
        Object.getOwnPropertyDescriptors(o);	// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
        Reflect.ownKeys(o);	// ["baz", "qux", Symbol(foo), Symbol(bar)]
        

    3.4.7.4 常用内置符号

    ECMAScript 6 引入了一批常用内置符号,用于暴露语言内部行为,开发者可以世界访问、重写或模拟这些行为。内置符号也就是全局函数 Symbol 的普通字符串属性,指向一个符号实例。

    • Symbol.hasInstance:一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用。

      • 在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系

        • function Foo() {}
          let f = new Foo();
          // 作用一致
          console.log(f instanceof Foo);	// true
          console.log(Foo[Symbol.hasInstance](f));	// true
          
      • 可以在继承的类上通过静态方法重新定义这个函数。

        • class F {}
          class C extends F {
            static [Symbol.hasInstance]() {
              return false;
            }
          }
          let c = new C();
          // 默认行为
          console.log(c instanceof F); // true
          console.log(F[Symbol.hasInstance](c)); // true
          // 通过重写内置符号定制行为
          console.log(c instanceof C); // false
          console.log(C[Symbol.hasInstance](c)); // false
          
    • Symbol.isConcatSpreadable:一个布尔值,如果是 true,则意味着对象应该使用 Array.prototype.concat() 打平其数组元素。

      • ES6 中的 Array.prototype.concat() 方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。下列覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为:

        • 数组对象
          • 默认情况下会被打平到已有的数组。
          • false 或假值会导致整个对象被追加到数组末尾。
        • 类数组对象
          • 默认情况会被追加到数组末尾。
          • true 或真值会导致这个类数组对象被打平到数组实例。
        • 其他不是类数组对象的对象
          • true 或真值将被忽略。
      • 代码演示上述情况:

        • // 数组对象
          let initial = ['foo'];
          let array = ['bar'];
                
          console.log(array[Symbol.isConcatSpreadable]);	// undefined
          console.log(initial.concat(array));	// ['foo', 'bar']
          array.[Symbol.isConcatSpreadable] = false;	// 改变内置符号
          console.log(initial.concat(array));	// ['foo', Array(1)]
          
        • // 类数组对象
          let initial = ['foo'];
          let arrayLikeObject = { length: 1, 0: 'baz' };
                
          console.log(arrayLikeObject[Symbol.isConcatSpreadable]);	// undefined
          console.log(initial.concat(arrayLikeObject));	// ['foo', {...}]
          arrayLikeObject.[Symbol.isConcatSpreadable] = true;	// 改变内置符号
          console.log(initial.concat(arrayLikeObject));	// ['foo', 'baz']
          
        • // 类数组对象
          let initial = ['foo'];
          let otherObject = new Set().add('qux')
                
          console.log(otherObject[Symbol.isConcatSpreadable]);	// undefined
          console.log(initial.concat(otherObject));	// ['foo', Set(1)]
          otherObject.[Symbol.isConcatSpreadable] = true;	// 改变内置符号
          console.log(initial.concat(otherObject));	// ['foo']
          
    • Symbol.asyncIterator:一个方法,该方法返回对象默认的 AsyncIterator。由 for-await-of 语句使用。

      • 这个由 Symbol.asyncIterator 函数生成的对象可以隐式通过异步生成器函数返回:

        • class Emitter {
            constructor(max) {
              this.max = max;
              this.asyncIdx = 0;
            }
            // 通过符号隐式修改 for-await-of 的默认行为
            async *[Symbol.asyncIterator](){
              while(this.asyncIdx < this.max) {
                yield new Promise((resolve) => resolve(this.asyncIdx++));
              }
            }
          }
                
          async function asyncCount() {
            let emitter = new Emitter(5);
            for await(const x of emitter) {
              console.log(x);
            }
          }
                
          asyncCount();
          // 0
          // 1
          // 2
          // 3
          // 4
          
    • Symbol.iterator:一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用

      • 这个由 Symbol.iterator 函数生成的对象可以隐式通过生成器函数返回:

        • class Emitter {
            constructor(max) {
              this.max = max;
              this.Idx = 0;
            }
            // 通过符号隐式修改 for-await-of 的默认行为
            async *[Symbol.iterator](){
              while(this.Idx < this.max) {
                yield this.Idx++;
              }
            }
          }
                
          function count() {
            let emitter = new Emitter(5);
            for (const x of emitter) {
              console.log(x);
            }
          }
                
          count();
          // 0
          // 1
          // 2
          // 3
          // 4
          
        • 迭代器后续相关内容将在第 7 章详细介绍。

    • Symbol.match:一个正则表达式方法,该方法用正则表达式去匹配字符串。由 prototype.match() 方法使用。

      • String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。

      • Symbol.match 函数接收一个参数,就是调用 match() 方法的字符串实例。返回的值没有限制:

        • class FooMatcher {
            static [Symbol.match](target) {
              return target.includes('foo');
            }
          }
          console.log('foobar'.match(FooMatcher));	// true
          console.log('barbaz'.match(FooMatcher));	// false
                
          class StringMatcher {
            constructor(str) {
              this.str = str
            }
          	[Symbol.match](target) {	// 重置 String.prototype.match() 方法
              return target.includes(this.str);
            }
          }
          console.log('foobar'.match(new StringMatcher('foo')));	// true
          console.log('barbaz'.match(new StringMatcher('qux')));	// false
                
          
    • Symbol.replace:一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace() 方法使用。

      • String.prototype.replace() 方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。

      • Symbol.replace 函数接收两个参数,即调用 replace() 方法字符串实例和替换字符串。返回的值没有限制:

        • class FooReplacer {
            static [Symbol.replace](target, replacement) {
              return target.split('foo').join(replacement);
            }
          }
          console.log('barfoobaz'.replace(FooReplacer, 'qux'));	// "barquxbaz"
                
          class StringReplacer {
            constructor(str) {
              this.str = str;
            }
            [Symbol.replace](target, replacement) {
              return target.split(this.str).join(replacement);
            }
          }
          console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));	// "barquxbaz"
                
          
    • Symbol.search:一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search() 方法使用。

      • String.prototype.search()方法会使用以 Symbol.search 为键的函数来对正则表达式求值。

      • Symbol.search 函数接收一个参数,就是调用 match() 方法的字符串实例。返回的值没有限制:

        • class FooSearcher {
            static [Symbol.search](target) {
              return target.indexOf('foo');
            }
          }
          console.log('foobar'.replace(FooSearcher));	// 0
          console.log('barfoo'.replace(FooSearcher));	// 3
          console.log('barbaz'.replace(FooSearcher));	// -1
                
          class StringSearcher {
            constructor(str) {
              this.str = str;
            }
            [Symbol.search](target) {
              return target.indexOf(this.str);
            }
          }
          console.log('foobar'.replace(new FooSearcher('foo')));	// 0
          console.log('barfoo'.replace(new FooSearcher('foo')));	// 3
          console.log('barbaz'.replace(new FooSearcher('qux')));	// -1
          
    • Symbol.species:一个函数值,该函数作为创建派生对象的构造函数。

      • 这个属性在内置类型中最常用,用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义:

        • class Bar extends Array {}
                
          class Baz extends Array {
            static get [Symbol.species]() {
              return Array;
            }
          }
                
          let bar = new Bar();
          console.log(bar instanceof Array);	// true
          console.log(bar instanceof Bar);	// true
          bar = bar.concat('bar');
          console.log(bar);	// Bar(1) [ 'bar' ]
          console.log(bar instanceof Array);	// true
          console.log(bar instanceof Bar);	// true
                
          let baz = new Baz();
          console.log(baz instanceof Array);	// true
          console.log(baz instanceof Baz);	// true
          baz = baz.concat('baz');
          console.log(baz);	// [ 'bar' ]
          console.log(baz instanceof Array);	// true
          // Symbol.species 内置重置
          console.log(baz instanceof Baz);	// false
          
    • Symbol.split:一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split() 方法使用。

      • String.prototype.split() 方法会使用以 Symbol.split 为键的函数来对正则表达式求值。

      • Symbol.split 函数接收一个参数,就是调用 match() 方法的字符串实例。返回的值没有限制:

        • class FooSplitter {
            static [Symbol.split](target) {
              return target.split('foo');
            }
          }
                
          console.log('barfoobaz'.split(FooSplitter));	// ["bar", "baz"]
                
          class StringSplitter{
            constructor(str) {
              this.str = str;
            }
                  
            [Symbol.split](target) {
              return target.split(this.str);
            }
          }
          console.log('barfoobaz'.split(new StringSplitter('foo')));	// ["bar", "baz"]
          
    • Symbol.toPrimitive:一个方法,该方法将对象转为相对应的原始值,由 ToPrimitive 抽象操作使用。

      • 根据提供给这个函数的参数(string、number 或 default),可以控制返回的原始值:

        • class Foo {}
          class Bar {
            constructor() {
              this[Symbol.toPrimitive] = function(hint) {
                switch (hint) {
                  case 'number':
                    return 3;
                  case 'string':
                    return 'string bar';
                  case 'default':
                  default:
                    return 'default bar';
                }
              }
            }
          }
                
          let foo = new Foo();
          console.log(3 + foo);	// "3[object Object]"
          console.log(3 - foo);	// NaN
          console.log(String(foo));	// "[object Object]"
                
          let bar = new Bar();
          console.log(3 + bar);	// "3default bar"
          console.log(3 - bar);	// 0
          console.log(String(bar));	// "string bar"
          console.log(Number(bar));	// 3
          
    • Symbol.toStringTag:一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString() 使用。

      • let s = new Set();
            
        class Foo {}
        let foo = new Foo();
            
        class Bar {
          constructor() {
            this[Symbol.toStringTag] = 'Bar';
          }
        }
        let bar = new Bar();
            
        console.log(s.toString());	// [object Set]
        console.log(s[Symbol.toStringTag]);	// Set
            
        console.log(foo.toString());	// [object Object]
        console.log(foo[Symbol.toStringTag]);	// Object
            
        console.log(bar.toString());	// [object Bar]
        console.log(bar[Symbol.toStringTag]);	// Bar
        

    3.4.8 Object 类型

    ECMAScript 中的对象其实就是一组数据和功能的集合。

    let o = new Object();
    let o2 = new Object;	// 合法,但不推荐
    

    Object 是派生其他对象的基类,Object 类型的所有属性和方法在派生的对象上同样存在,每个 Object 实例都有如下属性和方法。

    • constructor:用于创建当前对象的函数。
    • hasOwnProperty():用于判断当前对象实例(不是原型)上是否有存在给定的属性。传入字符串参数。
    • isPrototypeof():用于判断当前对象是否为另一个对象的原型。传入字符串参数。
    • propertyIsEnumerable():用于判断给定的属性是否可以使用 for-in 语句枚举。传入字符串参数。
    • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
    • toString():返回对象的字符串表示。
    • valueOf():返回对象对应的字符串、数值、布尔值表示。通常与 toString() 的返回值相同。

    3.5 操作符

    ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。在应用给对象时,操作符通常会调用 valueof() 或 toString() 方法来取得可以计算的值

    3.5.1 一元操作符

    • 递增/递减操作符:直接照搬自 C 语言,且可作用于任何值,整数、字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则:

      • 字符串

        • 有效的数值形式,转换为数值再应用改变。变量类型从字符串变成数值。
        • 无效的数值形式,将变量值设置为 NaN。变量类型从字符串变成数值。
      • 布尔值

        • false,转换为 0 再应用改变。变量类型从布尔值变成数值。
        • true,转换为 1 再应用改变。变量类型从布尔值变成数值。
      • 浮点值:直接加 1 或减 1。

      • 对象:调用其 valueof() 方法取得可以操作的值,对得到的值应用上述规则。如果是 NaN,则调用 toString() 并再次应用其他规则。变量类型从对象变为数值。

      • // 演示上述规则
        let s1 = "2";
        let s2 = "z";
        let b = false;
        let f = 1.1;
        let o = {
          valueof() {
            return -1;
          }
        }
            
        s1++;	// 值变成数值 3
        s2++;	// 值变成 NaN
        b++;	// 值变成数值 1
        f--;	// 值变成 0.10000000000000009(因为浮点数不精确)
        o--;	// 值变成 -2
        
    • 一元加和减:它们在 ECMAScript 中跟高中数学中的用途一样,可用于数据类型转换。

      • 一元加由一个加号(+)表示,放在变量前头,对数值没有影响。

        • // 演示上述规则
          let s1 = "01";
          let s2 = "1.1";
          let s3 = "z";
          let b = false;
          let f = 1.1;
          let o = {
            valueof() {
              return -1;
            }
          }
                
          s1 = +s1;	// 值变成数值 1
          s2 = +s2;	// 值变成 1.1
          s3 = +s3;	// 值变成 NaN
          b = +b;	// 值变成数值 0
          f = +f;	// 值变成 1.1
          o = +o;	// 值变成 -1
          
      • 一元减由一个减号(-)表示,放在变量前头将其变成相应的负值

        • // 演示上述规则
          let s1 = "01";
          let s2 = "1.1";
          let s3 = "z";
          let b = false;
          let f = 1.1;
          let o = {
            valueof() {
              return -1;
            }
          }
                
          s1 = -s1;	// 值变成数值 -1
          s2 = -s2;	// 值变成 -1.1
          s3 = -s3;	// 值变成 NaN
          b = -b;	// 值变成数值 0
          f = -f;	// 值变成 -1.1
          o = -o;	// 值变成 1
          

    3.5.2 位操作符

    ECMAScript 中的所有数值都是以 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先将值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。因此在进行位操作符时,只需要考虑 32 位整数即可。

    • 有符号整数前 31 位表示整数值,第 32 位表示数值的符号。0 表示正,1 表示负。

    • 无符号整数 32 位均表示整数值,因此比有符号整数的范围更大。

    3.5.2.1 按位非

    ~ 表示,其作用返回数值的二进制的取反,是二进制数学操作符之一。

    let num1 = 25;	// 二进制 00000000 00000000 00000000 00011001
    let num2 = ~num1;	// 二进制 11111111 11111111 11111111 11100110
    console.log(num2);	// -26
    
    // 按位非,即是对数值的二进制取反。
    // 最终效果对十进制数取反并减 1,如下操作。
    let num3 = 25;
    let num4 = -num3 - 1;
    console.log(num4);	// 26
    

    3.5.2.2 按位与

    & 表示,有两个操作数,若两个操作数二进制每一对应相同位上均为 1 则返回 1,否则返回 0。

    let result = 25 & 3;	// 1
    /**
     *  25 = 0000 0000 0000 0000 0000 0000 0001 1001
     *   3 = 0000 0000 0000 0000 0000 0000 0000 0011
     * AND = 0000 0000 0000 0000 0000 0000 0000 0001
     */
    
    let result2 = 25 & 20;	// 16
    /**
     *  25 = 0000 0000 0000 0000 0000 0000 0001 1001
     *  20 = 0000 0000 0000 0000 0000 0000 0001 0100
     * AND = 0000 0000 0000 0000 0000 0000 0001 0000
     */
    

    3.5.2.3 按位或

    | 表示,有两个操作符,返回两个操作数二进制每一对应相同位上有 1 则返回 1,否则返回 0。

    let result = 25 & 3;	// 27
    /**
     *  25 = 0000 0000 0000 0000 0000 0000 0001 1001
     *   3 = 0000 0000 0000 0000 0000 0000 0000 0011
     * AND = 0000 0000 0000 0000 0000 0000 0001 1011
     */
    

    3.5.2.4 按位异或

    ^ 表示,有两个操作符,返回两个操作数二进制每一对应相同位上数值不同则返回 1,否则返回 0。

    let result = 25 & 3;	// 26
    /**
     *  25 = 0000 0000 0000 0000 0000 0000 0001 1001
     *   3 = 0000 0000 0000 0000 0000 0000 0000 0011
     * AND = 0000 0000 0000 0000 0000 0000 0001 1010
     */
    

    3.5.2.5 左移

    << 表示,按照指定位数将数值的所有位向左移动,以 0 填充空位(保留 32 位中最左侧的符号位)。

    let oldValue = 2;	// 二进制 10
    let newValue = oldValue << 5;	// 二进制 1000000 => 64
    
    // 左移会保留符号位
    let oldValue2 = -2;	// 二进制 -10
    let newValue2 = oldValue2 << 5;	// 二进制 -1000000 => -64
    

    3.5.2.6 有符号右移

    >> 表示,按照指定位数将数值的所有位向右移动,同时保留符号(正或负),(保留 32 位中最左侧的符号位)。

    let oldValue = 64;	// 二进制 1000000
    let newValue = oldValue >> 5;	// 二进制 10 => 2
    
    // 左移会保留符号位
    let oldValue2 = -64;	// 二进制 -1000000
    let newValue2 = oldValue2 >> 5;	// 二进制 -10 => -2
    

    3.5.2.7 无符号右移

    >>> 表示,按照指定位数将数值的所有位向右移动,不保留符号,可表示的范围变大。

    • 正数:无符号右移与有符号右移结果相同。

      • let oldValue = 64;	// 二进制 1000000
        let newValue = oldValue >> 5;	// 二进制 10 => 2
        
    • 负数:无符号右移会给空位补 0(不保留符号位),因此结果将变得非常之大。

      • let oldValue = -64;	// 二进制 11111111 11111111 11111111 11000000,无符号的负数是其绝对值的二补数。
        let newValue = oldValue >>> 5;	// 二进制 00000111 11111111 11111111 11111110,十进制 134217726。
        

    3.5.3 布尔操作符

    布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或。

    • ! 逻辑非:该操作符首先将操作数转换为布尔值,然后再对其取反。

      • 对象,则返回 false。
      • 空字符串,则返回 true。
      • 非空字符串,则返回 false。
      • 数值 0,则返回 true。
      • 非 0 数值(包括 Infinity),则返回 false。
      • null,则返回 true。
      • NaN,则返回 true。
      • undefined,则返回 true。
      console.log(!false);	// true
      console.log(!"blue");	// false
      console.log(!0);	// true
      console.log(!NaN);	// true
      console.log(!"");	// true
      console.log(!12345);	// false
      
    • && 逻辑与:该操作符应用到两个值,且遵循如下规则:

      • 第一个操作数第二个操作数结果
        truetruetruetruefalsefalsefalsetruefalsefalsefalsefalse

        当第一个操作符为 true 时,将会作用到第二个操作符。反之不会作用到第二个操作符,因此可以当作一种短路操作符

        let found = true;
        let result = (found && undeclaredVariable);	// 作用到未定义的 undeclaredVariable,这里会出错
        console.log(result);	// 不会执行这一行
            
        let found2 = false;
        let result2 = (found2 && undeclaredVariable);	// 不会作用到未定义的 undeclaredVariable。
        console.log(result2);	// 会执行这一行,打印 false。
        
    • || 逻辑或:该操作符应用到两个值,且遵循如下规则:

      • 第一个操作数第二个操作数结果
        truetruetruetruefalsetruefalsetruetruefalsefalsefalse

        与逻辑与类似,逻辑或也具有段路的特性。当第一个操作数求值为 true,第二个操作数就不会再被求值。

        let found = false;
        let result = (found || undeclaredVariable);	// 作用到未定义的 undeclaredVariable,这里会出错
        console.log(result);	// 不会执行这一行
            
        let found2 = true;
        let result2 = (found2 && undeclaredVariable);	// 不会作用到未定义的 undeclaredVariable。
        console.log(result2);	// 会执行这一行,打印 false。
            
        // 利用这种行为,可以避免给变量赋值 null 或 undefined。
        let myObject = preferredObject || backupObject	// backupObject 可以被看作为备用的值。
        

    3.5.6 加性操作符

    加性操作符,即加法和减法操作符,在 ECMAScript 中,这两个操作符在后台会发生不同数据类型的转换。

    • + 加法操作符,用于求两个数的和,并应用如下转换规则:

      • 两个操作数均为数值
        • 任一操作数为 NaN,则返回 NaN。
        • Infinity 加 Infinity,则返回 Infinity。
        • -Infinity 加 -Infinity,则返回 -Infinity。
        • Infinity 加 -Infinity,则返回 NaN。
        • -0 加 +0,则返回 +0。
      • 两个操作数不均为数值
        • 均为字符串,则将第二个字符串拼接到第一个字符串后面。
        • 任一操作数为字符串,则将另一个操作数转换为字符串,拼接起来。
        • 任一操作数为其他数据类型,则调用它们的 toString() 方法,再拼接起来。
      // 上述常见错误:字符串与 num1 加法操作为字符串,再与 num2 加法操作仍为字符串。
      let num1 = 5;
      let num2 = 10;
      let message = "The sum of 5 and 10 is " + num1 + num2;
      console.log(message);	// "The sum of 5 and 10 is 510"
        
      // 解决:优先相加数值,再拼接为字符串。
      let num1 = 5;
      let num2 = 10;
      let message = "The sum of 5 and 10 is " + (num1 + num2);
      console.log(message);	// "The sum of 5 and 10 is 15"
      
    • - 减法操作符,用于求两个数的差,并应用如下转换规则:

      • 两个操作数均为数值

        • 任一操作数为 NaN,则返回 NaN。
        • Infinity 减 Infinity,则返回 NaN。
        • -Infinity 减 -Infinity,则返回 NaN。
        • Infinity 减 -Infinity,则返回 Infinity。
        • +0 减 -0,则返回 -0。
        • -0 减 -0,则返回 +0。
      • 任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number() 将其转换为数值,再根据应用上边规则。

      • 任一操作数是对象

        • 有valueOf()方法,则调用其 valueOf() 方法取得表示它的数值。
        • 无valueOf()方法,则调用其 toString() 方法,然后再将得到的字符串转换为数值。
        // 演示上述规则
        let res1 = 5 - true;	// 4
        let res2 = NaN - 1; //NaN
        let res3 = 5 - 3;	// 2
        let res4 = 5 - "";	// 5
        let res5 = 5 - "2";	// 3
        let res6 = 5 - null;	// 5
        

    3.5.7 关系操作符

    关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=),返回布尔值。应用到不同数据类型时也会发生类型转换和其他行为,如下规则:

    • 都是数值,则执行数值比较。
    • 都是字符串,则逐个比较字符串中对应字符的编码。
    • 任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
    • 有任一操作数是布尔值,则将其转换为数值再执行比较。
    • 任一操作数是对象
      • 若有 valueOf() 方法,取得结果后根据前面规则执行比较。
      • 若无 valueOf() 方法,则调用 toString() 方法,再根据前面规则执行比较。
    // 常见关系操作符错误
    
    // 1. 大写字母编码小于小写字母编码
    let resultError1 = "Brick" < "alphabet";	// true
    let resultGreat1 = "Brick".toLowerCase() < "alphabet".toLowerCase();	// false
    
    // 2. 数值字符串,字符编码不一致。
    let resultError2 = "23" < "3";	// true
    let resultGreat2 = "23" < 3;	// false
    
    // 3. 若关系操作符涉及到返回结果为 NaN,则均返回 false。
    let result1 = NaN < 3;	// false
    let result2 = NaN >= 3;	// false
    
    

    3.5.8 相等操作符

    ECMAScript 提供了两组操作符。第一组是等于不等于,在比较之前执行转换。第二组是全等不全等,在比较之前不执行转换。

    3.5.8.1 等于和不等于

    等于操作符用(==)表示,不等于操作符用(!=)表示。这两个操作符都会先进行强制类型转换再确定操作数是否相等。

    遵循如下转换规则:

    • 任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1。
    • 一个操作数是字符串,另一个是数值,则将字符串转换为数值,再比较是否相等。
    • 一个操作数是对象,另一个不是,则调用对象的 valueOf() 取得原始值,再根据前面规则进行比较。

    遵循如下比较规则:

    • null 和 undefined 相等。
    • null 和 undefined 不能转换为其他类型的值再进行比较。
    • 任一操作数为 NaN,则均为不相等。按照规则, NaN 不等于 NaN。
    • 两个操作符均为对象,则比较是否为同一对象,是则相等,反之则不相等。
    表达式结果
    null == undefinedtrue"NaN" == NaNfalse5 == NaNfalseNaN == NaNfalseNaN != NaNtruefalse == 0truetrue == 1truetrue == 2falseundefined == 0falsenull == 0false"5" == 5true

    3.5.8.2 全等和不全等

    全等操作符用(===)表示,不全等操作符用(!==)表示。这两个操作符都不会进行强制类型转换,便确定操作数是否相等。

    let res1 = ("55" == 55);	// true,转换后相等。
    let res2 = ("55" === 55);	// false,数据类型不同。
    
    let res3 = ("55" != 55);	// false,转换后相等。
    let res4 = ("55" !== 55);	// true,数据类型不同。
    
    let res5 = (null == undefined);	// true,两个值类似。
    let res6 = (null !== undefined);	// false,数据类型不同。
    

    3.5.9 条件操作符

    // boolean_expression 为 true 返回 true_value 赋予 variable,反之亦然。
    variable = boolean_expression ? true_value : false_value;
    

    3.5.11 逗号操作符

    // 逗号操作符可以用来在一条语句中执行多个操作。
    let num1 = 1, num2 = 2, num3 = 3;
    
    // 利用逗号操作符在赋值多个值时,最终会返回表达式中对后一个值。
    let num2 = (5, 1, 4, 8, 0);	// 0
    

    3.6 语句

    ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。

    3.6.2 do-while 语句

    do-while 语句是一种后测试循环语句,用于循环体内代码在退出前至少要执行一次。

    let i = 0;
    do {
      i += 2;
      console.log(i);
    } while (i < 10);
    
    // 2、4、6、8、10
    

    3.6.5 for-in 语句

    for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。如果要迭代的变量是 null 或 undefined,则不执行循环体。

    const obj = {
      name: "lindada",
      age: 21,
      job: null
    }
    
    for (const propName in obj) {	// const 确保这个局部变量不被修改。
      console.log(propName)	// 返回的属性顺序不一定一致。
    }
    // name、age、job
    

    3.6.6 for-of 语句

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

    for(const el of [2, 4, 6, 8]) {
      console.log(el);
    }
    // 2、4、6、8
    

    3.6.7 标签语句

    标签语句用于给语句加标签

    start: for (let i = 0; i < count; i++) {
      console.log(i);
    }
    
    // start 是 for 语句的标签,可以通过 break 或 continue 语句引用
    // 在嵌套循环中十分有用。
    

    3.6.8 break 和 continue 语句

    为执行循环代码提供了更严格的控制手段。break 语句用于立即跳出循环,强制执行循环后的下一语句。continue 语句也用于立即跳出循环,但会再次从循环顶部开始执行。

    // break 和 continue 与标签语句来控制外部嵌套循环的语句
    
    let num1 = 0;
    outermost1:
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
          break outermost1;
        }
        num1++;
      }
    }
    console.log(num1);	// 55
    // 当 i 和 j 均为 5 时,break 跳出结束外部标签 outermost 循环语句,结束循环。
    
    let num2 = 0;
    outermost2:
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
          continue outermost2;
        }
        num2++;
      }
    }
    console.log(num2);	// 95
    // 当 i 和 j 均为 5 时,continue 跳出结束外部标签 outermost 循环语句,进行下一次循环。
    

    3.6.10 switch 语句

    switch 语句是与 if 语句紧密相关的一种流控制语句,如下两个语句等效:

    const i = 25;
    if (i === 25) {
      console.log("25");
    } else if (i == 35) {
      console.log("35");
    } else {
      console.log("Other");
    }
    // 等效于
    switch (i) {
      case 25:
        console.log("25");
        break;
      case 35:
        console.log("35");
        break;
      default:
        console.log("Other");
    }
    
    

    每个 case 条件后面都需加上 break 语句,如果确实需要连续匹配几个条件,那么推荐写注释表明情况:

    const i = 25;
    switch (i) {
      case 25:
        /*跳过*/
      case 35:
        console.log("25 or 35");
      default:
        conosle.log(Other);
    }
    
    let num = 25;
    switch (true) {
      case num < 0:
        console.log("Less than 0");
        break;
      case num >= 0 && num <= 10:
        console.log("Between 0 and 10");
        break;
      default:
        console.log("More than 20");
    }
    

    3.7 函数

    ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。(第 10 章会更详细地介绍函数。)

    function sayHi(name, message) {
      console.log("Hello" + name + ", " + message);
    }
    // 通过函数名来调用函数
    sayHi("lindada", "how are you today?");	// "Hello lindada, how are you today?"
    
    • 除了 return 语句之外没有任何特殊声明表明该函数有返回值,如下:
    function sum(num1, num2) {
      return num1 + num2;
    }
    const result = sum(5, 10);	// 15
    
    • 只要碰到 return 语句,函数就会立即停止执行并退出。
    function sum2(num1, num2) {
      return num1 + num2;
      console.log("can't do it!");	// 不会执行
    }
    
    • return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined,这种用法最常用于提前终止函数执行。
    function sayHi(name, message) {
      return;	// 终止返回 undefined
      console.log("Hello" + name + ", " + message);	// 不会执行
    }
    

    3.8 小结

    JavaScript 的核心语言特性在 ECMA-262 中以伪语言 ECMAScript 的形式来定义。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。下面总结一下 ECMAScript 中的基本元素。

    • ECMAScript中的基本数据类型
      • ES6 之前
        • Undefined、Null、Boolean、Number、String
      • ES6 之后 ES10 之前
        • 新增 Symbol
      • ES10 之后
        • 新增 BigInt
    • ECMAScript 不区分整数和浮点数,只有 Number 一种数值数据类型。
    • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
    • 严格模式为这门语言中某些容易出错的部分施加了限制。
    • ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符。
    • 流控制语句大多是从其他语言中借鉴惹来的。

    ECMAScript 中的函数特点

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

    起源地下载网 » (第三章)我一字一行地重读红宝书

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元