Skip to content

js读书笔记-JS的语言基础

引言

任何语言的核心所描述的都是这门语言在最基本的层面上如何工作,涉及语法、操作符、数据类型 以及内置功能,在此基础之上才可以构建复杂的解决方案。如前所述,ECMA-262 以一个名为 ECMAScript 的伪语言的形式,定义了 JavaScript 的所有这些方面。

语法

区分大小写

简而言之就是 test 和 Test 是两个完全不同的变量

标识符

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

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

注释

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

js
单行注释以两个斜杠字符开头,如: 
// 单行注释

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

严格模式

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

js
    "use strict";

虽然看起来像个没有赋值给任何变量的字符串,但它其实是一个预处理指令。任何支持的 JavaScript 引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏 ECMAScript 3 语法。

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

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

严格模式会影响 JavaScript 执行的很多方面,因此本书在用到它时会明确指出来。所有现代浏览器 都支持严格模式。

语句

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

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

即使语句末尾的分号不是必需的,也应该加上。记着加分号有助于防止省略造成的问题,比如可以 避免输入内容不完整。此外,加分号也便于开发者通过删除空行来压缩代码(如果没有结尾的分号,只删除空行,则会导致语法错误)。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的 位置补上分号以纠正语法错误。

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

js
if (test) {
    test = false;
    console.log(test);
}

if 之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中 使用代码块,即使要执行的只有一条语句,如下例所示:

js
if(test)
    console.log(111) // 不推荐

if(test){
    console.log(1111)
}

在控制语句中使用代码块可以让内容更清晰,在需要修改代码时也可以减少出错的可能性。

关键字与保留字

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

js
break       do          case        else
catch       export      class       extends
const       finally     continue    for
debugger    function    this        default     
if          throw       delete      import      
try         in          typeof      instanceof    
var         new         void        return        
while       super       with        switch        
yield

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

变量

ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。

var

这里分享一个有意思的东西

js
function foo() {
    var a = 20
}
foo()
console.log(a) // 出错
js
function foo() {
    a = 20
}
foo()
console.log(a) // 20

感兴趣的可以看一下作用域链和调用栈

如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化):

js
var message = "hi",
    found = false,
    age = 29;
变量提升
js
function foo() {
    console.log(a) // undefined
    var age = 26
}
foo()

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

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

这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次 9 使用 var 声明同一个变量也没有问题:

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

let

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

js
if(true) {
    var a = 20;
    console.log(a); // 20
}
console.log(a); // 20
js
if(true) {
    var a = 20;
    console.log(a); // 20
}
console.log(a); // 报错

在这里,age 变量之所以不能在 if 块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let。

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

js
var name;
var name;

let age;
let age; //SyntaxError;标识符age已经声明过了

当然,JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明:

js
var name = 'Nicholas';
console.log(name);    // 'Nicholas'
if (true) {
    var name = 'Matt';
    console.log(name);  // 'Matt'
}

let age = 30;
console.log(age);    // 30
if (true) {
    let age = 26;
    console.log(age);  // 26
}

对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量, 它们只是指出变量在相关作用域如何存在。

js
var name;
let name; // SyntaxError

let age;
var age; // SyntaxError
暂时性死区

let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。

js
// name 会被提升 
console.log(name); // undefined 
var name = 'Matt';

// age 不会被提升
console.log(age); // ReferenceError:age 没有定义 
let age = 26;

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。

全局声明

与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声 明的变量则会)。

js
var name = 'Matt';
console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age);  // undefined

不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。

条件声明

在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合 并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同 时也就不可能在没有声明的情况下声明它。

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

就是说 let 不会提升变量,就只能在执行到该行代码的时候才检验前面是否声明,如果前面声明了,就会报错,当然 try catch 也不能解决,因为 try catch 里面声明 let 就相当于在他们的块作用域了。

for 循环中的 let 声明

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

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

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

js
for (let i = 0; i `< 5; ++i) { // 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
js
在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
    for (var i = 0; i < 5; ++i) {
        setTimeout(() =>`` console.log(i), 0)
}
// 你可能以为会输出0、1、2、3、4 // 实际上会输出5、5、5、5、5

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时 逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。

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

js
for (let i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// 会输出0、1、2、3、4

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

const

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

js
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 的限制。

js
const p = {};
p.name = 'a'; // 不报错

p = {}; // 报错

JavaScript 引擎会为 for 循环中的 let 声明分别创建独立的变量实例,虽然 const 变量跟 let 变 量很相似,但是不能用 const 来声明迭代变量(因为迭代变量会自增):

js
for (const i = 0; i `< 10; ++i) {} // TypeError:给常量赋值

不过,如果你只想用 const 声明一个不会被修改的 for 循环变量,那也是可以的。也就是说,每 次迭代只是创建一个新变量。这对 for-of 和 for-in 循环特别有意义:

js
let i = 0;
for (const j = 7; i `< 5; ++i) {
    console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
    console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
    console.log(value);
}
// 1, 2, 3, 4, 5

声明风格和最佳实践

  • 尽量不使用 var
  • 尽量先使用 const 在使用 let

数据类型

ECMAScript 有 6 种简单数据类型( 也称为原始类型 ):Undefined、Null、Boolean、Number、 String 和 Symbol。Symbol( 符号 )是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object( 对象 )。Object 是一种无序名值对的集合。

因为在 ECMAScript 中不能定义自己的数据类型,所有值都可以用上述 7 种数据类型之一来表示。只有 7 种数据类型似乎不足以表示全部数据。但 ECMAScript 的数 据类型很灵活,一种数据类型可以当作多种数据类型来使用。

typeof操作符

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

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

下面是使用 typeof 操作符的例子:

js
let message = "some string";
console.log(typeof message);
console.log(typeof(message));
console.log(typeof 95);
// "string"
// "string"
// "number"

在这个例子中,我们把一个变量(message)和一个数值字面量传给了 typeof 操作符。注意,因 为 typeof 是一个操作符而不是函数,所以不需要参数(但可以使用参数)。

注意 typeof 在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用 typeof null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。( js 会使用单位的低位表示类型,null 是 全为0,object 是 000, 而 undefined 是 -2^31( 即全为1 ) )

symbol

这里主要介绍的基本类型是 symbol:

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

尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为 Object API 提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。

符号的基本使用

调用 Symbol() 函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通 过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

js
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');

console.log(genericSymbol == otherGenericSymbol);  // false
console.log(fooSymbol == otherFooSymbol);  // false

注意,调用 Symbol() 函数时不能使用 new 关键字,这和 Number ,String 等是不同的

js
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

如果你确实想使用符号包装对象,可以借用 Object()函数:

js
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol);
// "object"

使用全局符号注册表

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

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

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

Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运 行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同 字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

js
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号 let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:

js
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

使用符号作为属性

类似于 Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnProperty- Symbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnProperty- Descriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型 的键:

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

这里可以分享一下 Object.keys 也是依赖于 getOwnPropertyNames 函数,并且会先数字后字符串的顺序输出,将所有合法的数组索引按升序的顺序存入,将所有字符串类型索引按属性创建时间以升序的顺序存入,有些回答说将所有属性为数字类型的 key 从小到大排序,其实不然,还必须要符合 「合法的数组索引」 ,也即只有正整数才行,负数或者浮点数,一律当做字符串处理。

PS:严格来说对象属性没有数字类型的,无论是数字还是字符串,都会被当做字符串来处理。

常用的内置符号

Symbol.asyncIterator

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

js
lass Emitter {
    constructor(max) {
    this.max = max;
    this.asyncIdx = 0;
    }
    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.hasInstance

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

在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。以 Symbol. hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下:

js
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {} 
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true

这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof 操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通 过静态方法重新定义这个函数:

js
class Bar {}
class Baz extends Bar {
    static [Symbol.hasInstance]() {
        return false;
    }
}
let b = new Baz(); 
console.log(Bar[Symbol.hasInstance](b)); // true 
console.log(b instanceof Bar); // true 
console.log(Baz[Symbol.hasInstance](b)); // false 
console.log(b instanceof Baz); // false
Symbol.isConcatSpreadabl

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

数组对象默认情况下会被打平到已有的数组,false 或假值会导致整个对象被追加到数组末尾。类 数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例。其 他不是类数组对象的对象在 Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略。

js
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 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 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.iterator

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

for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API 的 Generator:

技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next()方法陆续返回值。可以通 过显式地调用 next()方法返回,也可以隐式地通过生成器函数返回:

js
class Emitter {
    constructor(max) {
        this.max = max;
        this.idx = 0;
        }
        *[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
Symbol.match

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

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让 match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的 字符串实例。

js
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) {
        return target.includes(this.str);
    }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true 
console.log('barbaz'.match(new StringMatcher('qux'))); // false
Symbol.replace

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

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,可以重新定义 Symbol.replace 函数以取代默认对正则表达式求值的行为,从而让 replace()方法使用非正则表达式实例。Symbol.replace 函数接收两个参数,即调用 replace() 方法的字符串实例和替换字符串。返回的值没有限制:

js
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"

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

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,可以重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为,从而让 search()方法使用非正则表达式实例。Symbol.search 函数接收一个参数,就是调用 match()方法 的字符串实例。返回的值没有限制:

js
class FooSearcher {
    static [Symbol.search](target) {
        return target.indexOf('foo');
    }
}
console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1

class StringSearcher {
  constructor(str) {
    this.str = str;
  }
  [Symbol.search](target) {
    return target.indexOf(this.str);
}
}
console.log('foobar'.search(new StringSearcher('foo'))); // 0 
console.log('barfoo'.search(new StringSearcher('foo'))); // 3 
console.log('barbaz'.search(new StringSearcher('qux'))); // -1
Symbol.species

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

js
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 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 instanceof Array); // true 
console.log(baz instanceof Baz); // false

console.log(baz.map(x =>` x) instanceof Baz); // false
Symbol.split

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

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,可以重新定义 Symbol.split 函数以取代默认对正则表达式求值的行为,从而让 split() 方法使用非正则表达式实例。Symbol.split 函数接收一个参数,就是调用 match()方法的字符串实 例。返回的值没有限制:

js
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

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

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

js
class Foo {}
let foo = new Foo();
console.log(3 + foo);// "3[object Object]"
console.log(3 - foo);// NaN
console.log(String(foo));// "[object Object]"

class Bar {
    constructor() {
        this[Symbol.toPrimitive] = function(hint) {
            switch (hint) {
                case 'number':
                    return 3;
                case 'string':
                    return 'string bar';
                default:
                    return 'default bar';
            }
        }
    } 
}
let bar = new Bar();
console.log(3 + bar);     // "3default bar"
console.log(3 - bar);     // 0
console.log(String(bar)); // "string bar"

ps: 这里可以看出 + 这个符号很有意思,后面会有介绍。

Symbol.toStringTag

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

通过 toString()方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默 认为"Object"。内置类型已经指定了这个值,但自定义类实例还需要明确定义:

js
let s = new Set();
console.log(s);                      // Set(0) {}
console.log(s.toString());           // [object Set]
console.log(s[Symbol.toStringTag]);  // Set

class Foo {}
let foo = new Foo();
console.log(foo);// Foo {}
console.log(foo.toString());// [object Object]
console.log(foo[Symbol.toStringTag]);  // undefined

class Bar {
    constructor() {
        this[Symbol.toStringTag] = 'Bar';
    } 
}
let bar = new Bar();
console.log(bar);// Bar {[Symbol(Symbol.toStringTag)]: 'Bar'}
console.log(bar.toString());// [object Bar]
console.log(bar[Symbol.toStringTag]);  // Bar
Symbol.unscopables

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

js
let o = { foo: 'bar' };
with (o) {
  console.log(foo); // bar
}

o[Symbol.unscopables] = { 
    foo: true
};
with (o) {
  console.log(foo); // ReferenceError
}

Object

ECMAScript 中的 Object 是派生其他对象的基类。Object 类型的所有属性和方法在派生的对象上同样存在。

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

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

注意 严格来讲,ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。

操作符

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

一元操作符

递增/递减操作符

主要就是前缀递增/递减和后缀递增/递减,其差别是后缀版递增和递减在语句被求值后才发生。

这 4 个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可 以。递增和递减操作符遵循如下规则:

  • 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
  • 对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成数值。
  • 对于布尔值,如果是 false,则转换为 0 再应用改变。变量类型从布尔值变成数值。
  • 对于布尔值,如果是 true,则转换为 1 再应用改变。变量类型从布尔值变成数值。
  • 对于浮点值,加 1 或减 1。
  • 如果是对象,则调用其(第 5 章会详细介绍的)valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成数值。

下面的例子演示了这些规则:

js
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 中跟在高中数学中的用途一 样。一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响。

如果将一元加应用到非数值,则会执行与使用 Number()转型函数一样的类型转换:布尔值 false 和 true 转换为 0 和 1,字符串根据特殊规则进行解析,对象会调用它们的 valueOf()和/或 toString() 方法以得到可以转换的值。 下面的例子演示了一元加在应用到不同数据类型时的行为:

js
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

对数值使用一元减会将其变成相应的负值(如上面的例子所示)。在应用到非数值时,一元减会遵 循与一元加同样的规则,先对它们进行转换,然后再取负值。

位操作符

位操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript 12 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因 为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。

特殊值 NaN 和 Infinity 在位操作中都会被当成 0 处理。如果将位操作符应用到非数值,那么首先会使用 Number()函数将该值转换为数值(这个过程是自 动的),然后再应用位操作。最终结果是数值。

按位非

按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数。按位非是 ECMAScript 中为数 不多的几个二进制数学操作符之一。看下面的例子:

js
let num1 = 25; // 二进制 00000000000000000000000000011001 
let num2 = ~num1; // 二进制 11111111111111111111111111100110 
console.log(num2); // -26

这里,按位非操作符作用到了数值 25,得到的结果是 26。由此可以看出,按位非的最终效果是对 数值取反并减 1,就像执行如下操作的结果一样:

js
let num1 = 25;
let num2 = -num1 - 1;
console.log(num2);   // "-26"

实际上,尽管两者返回的结果一样,但位操作的速度快得多。这是因为位操作是在数值的底层表示 上完成的。

按位与

按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐, 然后基于真值表中的规则,对每一位执行相应的与操作。

按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0。 下面看一个例子,我们对数值 25 和 3 求与操作,如下所示:

js
let result = 25 & 3;
console.log(result); // 1

按位或

按位或操作符用管道符(|)表示,同样有两个操作数。 按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0。 仍然用按位与的示例,如果对 25 和 3 执行按位或,代码如下所示:

js
let result = 25 | 3;
console.log(result); // 27

按位异或

按位异或用脱字符(^)表示,同样有两个操作数

按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0)。 对数值 25 和 3 执行按位异或操作:

js
let result = 25 ^ 3;
console.log(result); // 26

左移

左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。比如,如果数 值 2(二进制 10)向左移 5 位,就会得到 64(二进制 1000000),如下所示:

js
let oldValue = 2; // 等于二进制 10
let newValue = oldValue `<< 5; // 等于二进制 1000000,即十进制 64

注意,左移会保留它所操作数值的符号。比如,如果 -2 左移 5 位,将得到 -64,而不是正 64。

这个操作就像乘2的n次幂

有符号右移

有符号右移由两个大于号(>```>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。 有符号右移实际上是左移的逆运算。比如,如果将 64 右移 5 位,那就是 2:

js
let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2

注意,左移会保留它所操作数值的符号。比如,如果-2 左移 5 位,将得到-64,而不是正 64。

这个操作就像除2的n次幂

无符号右移

无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移。对于正数,无符号右移与 有符号右移结果相同。

对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补 0,而不管符号位是什么。

js
let oldValue = -64; // 等于二进制 11111111111111111111111111000000 
let newValue = oldValue >>> 5; // 等于十进制 134217726

在对-64 无符号右移 5 位后,结果是 134 217 726。这是因为64 的二进制表示是 1111111111111111111 1111111000000,无符号右移却将它当成正值,也就是 4 294 967 232。把这个值右移 5 位后,结果是 00000111111111111111111111111110,即 134 217 726。

布尔操作符

布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或。也就是 !,&&,||

请注意 && 和 || 如果经常可以用来赋值,二者都是短路操作符

  • 前者在第一个为 true 的时候直接返回第二个,后者在第一个为 true 就返回第一个
  • 前者在第一个为 false 的时候返回第一个,后者则是返回第二个

举个🌰

js
let found = true;
let result = (found && someUndeclaredVariable); // 这里会出错 console.log(result); // 不会执行这一行

let found = false;
let result = (found && someUndeclaredVariable); // 不会出错 console.log(result); // 会执行

let found = true;
let result = (found || someUndeclaredVariable); // 不会出错 console.log(result); // 会执行

let found = false;
let result = (found || someUndeclaredVariable); // 这里会出错 console.log(result); // 不会执行这一行

利用这个行为,可以避免给变量赋值 null 或 undefined。比如:

js
let myObject = preferredObject || backupObject;

在这个例子中,变量 myObject 会被赋予两个值中的一个。其中,preferredObject 变量包含首 选的值,backupObject 变量包含备用的值。如果 preferredObject 不是 null,则它的值就会赋给 myObject;如果 preferredObject 是 null,则 backupObject 的值就会赋给 myObject。这种模 式在 ECMAScript 代码中经常用于变量赋值,本书后面的代码示例中也会经常用到。

乘性操作符

ECMAScript 定义了 3 个乘性操作符:乘法、除法和取模。这些操作符跟它们在 Java、C 语言及 Perl 中对应的操作符作用一样,但在处理非数值时,它们也会包含一些自动的类型转换。如果乘性操作符有 不是数值的操作数,则该操作数会在后台被使用 Number()转型函数转换为数值。这意味着空字符串会 被当成 0,而布尔值 true 会被当成 1。

总而言之就是乘法( * ), 除法( / ), 取模( % )

几个注意点:

  • 如果是 Infinity 乘以 0,则返回 NaN。
  • 如果是 Infinity 乘以非 0 的有限数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。
  • 如果是 Infinity 乘以 Infinity,则返回 Infinity。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 除以 Infinity,则返回 NaN。
  • 如果是 0 除以 0,则返回 NaN。
  • 如果是非 0 的有限值除以 0,则根据第一个操作数的符号返回 Infinity 或-Infinity
  • 如果被除数是无限值,除数是有限值,则返回 NaN。 - 取模
  • 如果被除数是有限值,除数是 0,则返回 NaN。- 取模

指数操作符

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

加性操作符

加性操作符,即加法和减法操作符,一般都是编程语言中最简单的操作符。不过,在 ECMAScript 中,这两个操作符拥有一些特殊的行为。与乘性操作符类似,加性操作符在后台会发生不同数据类型的 转换。只不过对这两个操作符来说,转换规则不是那么直观。

加法操作符

加法操作符(+)用于求两个数的和,比如: let result = 1 + 2;

如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:

  • 如果有任一操作数是 NaN,则返回 NaN;
  • 如果是 Infinity 加-Infinity,则返回 NaN;
  • 如果是-0 加+0,则返回+0;
  • 如果是-0 加-0,则返回-0。 不过,如果有一个操作数是字符串,则要应用如下规则:
  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。

如果有任一操作数是对象、数值或布尔值,则调用它们的 toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于 undefined 和 null,则调用 String()函数,分别获取 "undefined"和"null"。

看下面的例子:

js
let result1 = 5 + 5;// 两个数值
console.log(result1);// 10
let result2 = 5 + "5";// 一个数值和一个字符串
console.log(result2);// "55"

以上代码展示了加法操作符的两种运算模式。正常情况下,5 + 5 等于 10(数值),如前两行代码 所示。但是,如果将一个操作数改为字符串,比如"5",则相加的结果就变成了"55"(原始字符串值), 因为第一个操作数也会被转换为字符串。

ECMAScript 中最常犯的一个错误,就是忽略加法操作中涉及的数据类型。比如下面这个例子:

js
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 result = 2 - 1;

与加法操作符一样,减法操作符也有一组规则用于处理 ECMAScript 中不同类型之间的转换。

  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 减 Infinity,则返回 NaN。
  • 如果是-Infinity 减-Infinity,则返回 NaN。
  • 如果是 Infinity 减-Infinity,则返回 Infinity。
  • 如果是-Infinity 减 Infinity,则返回-Infinity。
  • 如果是+0 减+0,则返回+0。
  • 如果是+0 减-0,则返回-0。
  • 如果是-0 减-0,则返回+0。
  • 如果有任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是 NaN,则减法计算的结果是NaN。
  • 如果有任一操作数是对象,则调用其 valueOf()方法取得表示它的数值。如果该值是 NaN,则减法计算的结果是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法,然后再将得到的字符串转换为数值。

关系操作符

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

  • 任何关系操作符在涉及比较 NaN 时都返回 false
  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其 valueOf()方法,取得结果后再根据前面的规则执行比较。 如果没有 valueOf()操作符,则调用 toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较。

在使用关系操作符比较两个字符串时,会发生一个有趣的现象。很多人认为小于意味着“字母顺序 靠前”,而大于意味着“字母顺序靠后”,实际上不是这么回事。对字符串而言,关系操作符会比较字符 串中对应字符的编码,而这些编码是数值。比较完之后,会返回布尔值。问题的关键在于,大写字母的 编码都小于小写字母的编码,因此以下这种情况就会发生:

js
let result = "Brick" `< "alphabet"; // true

在这里,字符串"Brick"被认为小于字符串"alphabet",因为字母 B 的编码是 66,字母 a 的编码 是 97。要得到确实按字母顺序比较的结果,就必须把两者都转换为相同的大小写形式(全大写或全小写), 然后再比较:

js
let result = "Brick".toLowerCase() `< "alphabet".toLowerCase(); // false

将两个操作数都转换为小写,就能保证按照字母表顺序判定"alphabet"在"Brick"前头。 另一个奇怪的现象是在比较两个数值字符串的时候,比如下面这个例子:

js
let result = "23" `< "3"; // true

这里在比较字符串"23"和"3"时返回 true。因为两个操作数都是字符串,所以会逐个比较它们的 字符编码(字符"2"的编码是 50,而字符"3"的编码是 51)。不过,如果有一个操作数是数值,那么比较的结果就对了:

js
let result = "23" < 3; // false

因为这次会将字符串"23"转换为数值 23,然后再跟 3 比较,结果当然对了。只要是数值和字符串 比较,字符串就会先被转换为数值,然后进行数值比较。对于数值字符串而言,这样能保证结果正确。 但如果字符串不能转换成数值呢?比如下面这个例子:

js
let result = "a" < 3; // 因为"a"会转换为 NaN,所以结果是 false

因为字符"a"不能转换成任何有意义的数值,所以只能转换为 NaN。这里有一个规则,即任何关系操作符在涉及比较 NaN 时都返回 false。这样一来,下面的例子有趣了:

js
let result1 = NaN < 3;  // false
let result2 = NaN >```= 3; // false

在大多数比较的场景中,如果一个值不小于另一个值,那就一定大于或等于它。但在比较 NaN 时, 无论是小于还是大于等于,比较的结果都会返回 false。

相等操作符

判断两个变量是否相等是编程中最重要的操作之一。在比较字符串、数值和布尔值是否相等时,过 程都很直观。但是在比较两个对象是否相等时,情形就比较复杂了。ECMAScript 中的相等和不相等操 作符,原本在比较之前会执行类型转换,但很快就有人质疑这种转换是否应该发生。最终,ECMAScript提供了两组操作符。第一组是等于和不等于,它们在比较之前执行转换。第二组是全等和不全等,它们在比较之前不执行转换。

等于和不等于

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

在转换操作数的类型时,相等和不相等操作符遵循如下规则:

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

在进行比较时,这两个操作符会遵循如下规则:

  • null 和 undefined 相等。
  • null 和 undefined 不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象则相等操作符返回 true。否则,两者不相等。

下表总结了一些特殊情况及比较的结果。

表达式结果
null == undefinedtrue
"NaN" == NaNfalse
5 == NaNfalse
NaN == NaNfalse
NaN != NaNtrue
false == 0true
true == 1true
true == '1'true
true == 2false
undefined == 0false
null == 0false
"5" == 5true

全等和不全等

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

注意 由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。 这样有助于在代码中保持数据类型的完整性。

条件操作符

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

js
variable = boolean_expression ? true_value : false_value;

赋值操作符

就是 =、+=等等

逗号操作符

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

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

在一条语句中同时声明多个变量是逗号操作符最常用的场景。不过,也可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:

js
let num = (5, 1, 4, 8, 0); // num的值为0

在这个例子中,num 将被赋值为 0,因为 0 是表达式中最后一项。

语句

ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。 语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,复杂的如列出一堆要重复执行的指令。

if语句

if 语句是使用最频繁的语句之一,语法如下:

js
if (condition1) statement1 else if (condition2) statement2 else statement3

do-while语句

do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句 话说,循环体内的代码至少执行一次。do-while 的语法如下:

js
do {
    statement
} while (expression);

while语句

while 语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while 循 环体内的代码有可能不会执行。下面是 while 循环的语法:

js
while(expression) statement

for语句

for 语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,语法如下:

js
for (initialization; expression; post-loop-expression) statement

for-in语句

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

js
for (property in expression) statement

for-of语句

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

js
for (property of expression) statement

for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素。

标签语句

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

js
label: statement

下面是一个例子:

js
let num = 0; 
outermost:
for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            break outermost;
        }
        num++; 
    }
}
console.log(num); // 55

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

break和continue语句

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

with语句

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

js
with (expression) statement;

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

js
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;

上面代码中的每一行都用到了 location 对象。如果使用 with 语句,就可以少写一些代码:

js
with(location) {
    let qs = search.substring(1);
    let hostName = hostname;
    let url = href;
}

这里,with 语句用于连接 location 对象。这意味着在这个语句内部,每个变量首先会被认为是 一个局部变量。如果没有找到该局部变量,则会搜索 location 对象,看它是否有一个同名的属性。如 果有,则该变量会被求值为 location 对象的属性。

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

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

switch语句

switch 语句是与 if 语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript 中 switch 语句跟 C 语言中 switch 语句的语法非常相似,如下所示:

js
switch (expression) {
    case value1:
        statement
        break;
    case value2:
        statement
        break;
    default:
        statement 
}

这里的每个 case(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。”break 关键字会导致代码执行跳出 switch 语句。如果没有 break,则代码会继续匹配下一个条件。default 关键字用于在任何条件都没有满足时指定默认执行的语句(相当于 else 语句)。

注意 switch语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类 型(比如,字符串"10"不等于数值 10)。

函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。

ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。

小结

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

  • ECMAScript 中的基本数据类型包括 Undefined、Null、Boolean、Number、String 和 Symbol。
  • 与其他语言不同,ECMAScript 不区分整数和浮点值,只有 Number 一种数值数据类型。
  • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
  • 严格模式为这门语言中某些容易出错的部分施加了限制。
  • ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、 关系操作符、相等操作符和赋值操作符等。
  • 这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如 if 语句、for 语句和 switch 语句等。
  • ECMAScript 中的函数与其他语言中的函数不一样。
    • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
    • 不指定返回值的函数实际上会返回特殊值 undefined。

备案号:闽ICP备2024028309号-1