《JS高级程序设计》第3章 语言基础

90 阅读14分钟

第3章 语言基础

一、语法

  1. ECMAScript 中一切都区分大小写。如typeof是关键字,无法设置为变量名,但是Typeof可以设置为变量名。

  2. 标识符的要求是首字符需要为字母、下划线、美元符号,标识符的非首字符可以为字母、下划线、美元符号、数字。

  3. 标识符应采用驼峰大小写的形式,即第一个单词的首字母小写,后面每个单词的首字母大写,这跟 ECMAScript 内置函数和对象的命名方式一致。

  4. ES5 增加了严格模式,规避了 ES3 的部分不规范写法。使用"use strict";在脚本或函数体的首行可以开启严格模式。

  5. 省略分号意味着由解析器确定语句在哪里结尾,加分号的作用:

    1. 有助于防止省略造成的问题,避免输入内容不完整;
    2. 有助于删除空行以压缩代码行数的同时避免语法错误;
    3. 有助于提升性能,因为解析器会尝试补上分号以避免语法错误;

二、变量

  1. 变量是一个用于保存任意值的命名占位符,是松散类型的,这意味着变量可以保存任意类型的数据。有 3 个关键字可以声明变量:varconstlet
  2. 变量不初始化会保存一个特殊值undefined
  3. 使用var定义会使变量成为包含它的函数的局部变量,该变量在函数退出时会被销毁。
function test() {
	var message = 1; // 局部变量
}
test();
console.log(message); // Uncaught ReferenceError: a is not defined
  1. 在函数内定义变量省略var,会使该变量成为全局变量。
function test() {
	message = 1; // 全局变量
}
test();
console.log(message); // 1
  1. var声明的变量会自动提升到函数作用域的顶部,即把所有变量声明都拉到函数作用域的顶部。
function test() {
	console.log(message);
	var message = 1;
}
test(); // undefined

// 等同于
function test() {
	var message;
	console.log(message);
	message = 1;
}
test(); // undefined
  1. letvar的区别之一是,let声明的是块作用域,var声明的是函数作用域。块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let
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 没有定义
  1. let不允许同一个块中出现冗余声明。嵌套使用相同的标识符不会报错,因为在同一个块中没有重复声明。
var name;
var name;
let age;
let age; // SyntaxError: 标识符age已经声明过了
  1. 声明冗余的报错不会受到letvar混用的影响,这两个关键字只是指出变量在相关作用域中如何存在。
  2. let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError
  3. for循环中var定义的迭代变量会渗透到循环体外部,let定义的迭代变量的作用域仅限于for循环块内部。
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
} 
// 5 5 5 5 5
// 因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
} 
// 0 1 2 3 4
// 使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。 每个 setTimeout 引用的都是不同的变量实例。
  1. 总结letvar的区别:

    1. let声明的是块作用域,var声明的是函数作用域。
    2. let声明的变量不会在作用域中被提升。
    3. 使用let在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)。
  2. constlet的区别是用const声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。const声明的限制只适用于它指向的变量的引用。

// 用 const 声明一个不会被修改的 for 循环变量对 for-of 和 for-in 循环特别有意义
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
  1. 声明风格及最佳实践:

    1. 不使用var
    2. const优先,let次之

三、数据类型

  1. ECMAScript 有六种简单数据类型,也称为原始类型:UndefinedNullBooleanNumberStringSymbol。还有一种复杂数据类型叫Object(对象)。

  2. 调用typeof null返回的是"object"。这是因为特殊值null被认为是一个对空对象的引用。

  3. 函数有自己特殊的属性。为此有必要通过typeof操作符来区分函数和其他对象。

  4. Undefined类型只有一个值,就是特殊值undefined。增加undefined的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。

  5. Null类型同样只有一个值,即特殊值nullnull值表示一个空对象指针。

  6. 任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用null来填充该变量。

  7. 要将一个其他类型的值转换为布尔值,可以调用特定的Boolean()转型函数,if等流控制语句会自动执行其他类型值到布尔值的转换。

  8. 存在0.1+0.2≠0.3这种舍入错误,是因为使用了IEEE754数值,因此永远不要测试某个特定的浮点值。

  9. 任何涉及NaN的操作始终返回NaNNaN不等于包括NaN在内的任何值,任何不能转换为数值的值都会导致isNaN()函数返回true

  10. 【补充】Object.is(NaN, NaN)可以判断两个NaN相等。

  11. 有三个函数可以将非数值转换为数值:Number()parseInt()parseFloat()

  12. 使用parseInt()转换空字符串会返回NaNNumber()会返回 0。

  13. 不传底数参数相当于让parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给 它第二个参数。

  14. 因为parseFloat()只解析十进制值,因此不能指定底数。

  15. 以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。

  16. ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。

  17. 把一个值转换为字符串的方法有两种:toString()String()

  18. toString()方法可见于数值、布尔值、对象和字符串值,nullundefined值没有toString()方法。

  19. 如果不确定一个值是不是nullundefined,可以使用String()转型函数。

    1. 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
    2. 如果值是null,返回"null"
    3. 如果值是undefined,返回"undefined"
  20. 模板字面量保留换行字符,可以跨行定义字符串。模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后得到的是字符串。字符串插值通过在${}中使用一个 JavaScript 表达式实现。

  21. 字符串插值将表达式转换为字符串时会调用toString()

let foo = { toString: () => 'World' };
console.log(`Hello, ${foo}!`); // Hello, World!
  1. Symbol(符号)是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
  2. Symbol()函数不能与new关键字一起作为构造函数使用,这是为了避免创建符号包装对象。
  3. 如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此,需要使用Symbol.for()方法。
  4. 全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。
  5. Symbol.keyFor()可查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。如果传给Symbol.keyFor()的不是符号,则该方法抛出TypeError
  6. 对象字面量只能在计算属性语法中使用符号作为属性。
  7. 常用内置符号用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。重新定义内置符号可以改变原生结构的行为。
  8. Symbol.asyncIterator作为属性表示“一个方法,该方法返回对象默认的AsyncIterator。 由for-await-of语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。
class Foo {
  async *[Symbol.asyncIterator]() { }
}
let foo = new Foo();
console.log(foo[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}

class 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 (let x of emitter) {
    console.log(x);
  }
}
asyncCount();
// 0 1 2 3 4
  1. Symbol.hasInstance作为属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用“。在 ES6 中,instanceof操作符会使用Symbol.hasInstance函数来确定关系。
class Bar{};
let bar = new Bar();
console.log(bar instanceof Bar); // true
console.log(Bar[Symbol.hasInstance](bar)); // true

class Baz extends Bar {
  // 在继承的类上通过静态方法重新定义函数
  static [Symbol.hasInstance]() {
    return false;
  }
}
let baz = new Baz();
console.log(baz instanceof Bar); // true
console.log(Bar[Symbol.hasInstance](baz)); // true
console.log(baz instanceof Baz); // false
console.log(Baz[Symbol.hasInstance](baz)); // false
  1. Symbol.isConcatSpreadable作为属性表示“一个布尔值,如果是true,则意味着对象应该用 Array.prototype.concat()打平其数组元素”
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
// 数组对象默认情况下会被打平到已有的数组
console.log(initial.concat(array)); // ['foo', 'bar'] 
array[Symbol.isConcatSpreadable] = false;
// 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; 
// 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;
// true 的情况下不是类数组对象的对象将被忽略
console.log(initial.concat(otherObject)); // ['foo']
  1. Symbol.iterator作为属性表示“一个方法,该方法返回对象默认的迭代器。 由for-of语句使用”,换句话说,这个符号表示实现迭代器 API 的函数。
class Foo {
  *[Symbol.iterator]() { };
}
let f = new Foo();
console.log(f[Symbol.iterator]());
// Generator {<suspended>}
  1. Symbol.match作为属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”。Symbol.match函数接收一个参数,就是调用match()方法的字符串实例,返回的值没有限制。
console.log(RegExp.prototype[Symbol.match]);
// ƒ [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: undefined]

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
  1. Symbol.replace作为属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”。
console.log(RegExp.prototype[Symbol.replace]);
// ƒ [Symbol.replace]() { [native code] } 
console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz'

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"
  1. Symbol.search作为属性表示“表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用“。
  2. 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 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
  1. Symbol.split作为属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。
  2. Symbol.toPrimitive作为属性表示“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive抽象操作使用”。通过在这个实例的Symbol.toPrimitive属性将对象转换为原始值。
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';
        case 'default':
        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"
  1. Symbol.toStringTag作为属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用”。通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符。
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 {}
console.log(bar.toString());          // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar
  1. ECMAScript 中的对象其实是一组数据和功能的集合,是派生其他对象的基类。Object类型的所有属性和方法在派生的对象上同样存在。

四、操作符

  1. 在应用给对象时,操作符通常会调用valueOf()和/或toString()方法来取得可以计算的值,
  2. 后缀版与前缀版的主要区别在于,后缀版递增和递减在语句被求值后才发生。
  3. 同时使用两个叹号(!!),相当于调用了转型函数Boolean()
  4. 逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。
  5. 逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。利用这个行为,可以避免给变量赋值nullundefined
  6. 加法操作符中,如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
  7. 只要是数值和字符串比较,字符串就会先被转换为数值,然后进行数值比较。
  8. 等于和不等于在比较之前执行强制类型转换,全等和不全等在比较之前不执行类型转换。
  9. 在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值。
let num = (5, 1, 4, 8, 0); // num的值为0

五、语句

  1. 使用let声明迭代器变量,这样就可以将这个变量的作用域限定在循环中。
  2. for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。为了确保局部的迭代器变量不被修改,推荐使用const声明 。ECMAScript 中对象的属性是无序的,因此for-in语句不能保证返回对象属性的顺序。如果for-in循环要迭代的变量是nullundefined,则不执行循环体。
  3. for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素。为了确保局部的迭代器变量不被修改,推荐使用const声明。for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素。
  4. 标签语句的典型应用场景是嵌套循环。
  5. break语句用于立即退出循环,强制执行循环后的下一条语句。
  6. continue语句也用于立即退出循环,但会再次从循环顶部开始执行。
  7. switch语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型。

六、函数

  1. 只要碰到return语句,函数就会立即停止执行并退出。
  2. 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  3. 不指定返回值的函数实际上会返回特殊值undefined