前端从零开始第八、九周

138 阅读41分钟

第三十三天

1. 数组扩展

扩展运算符的使用

扩展运算符是三个点(...),将一个数组转为一用逗号隔开的参数序列(好比rest参数的逆运算)

console.log(...[1, 2, 3])
// 1 2 3
​
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]//扩展运算符后面是一个空数组,则不产生任何效果。
[...[], 1]
// [1]//只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
(...[1, 2])
// Uncaught SyntaxError: Unexpected number
​
console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number
​
console.log(...[1, 2])
// 1 2

该运算符主要用于函数调用。

function push(array, ...items) {
  array.push(...items);
}
​
function add(x, y) {
  return x + y;
}
​
const numbers = [4, 38];
add(...numbers) // 42

扩展运算符的应用

1. 复制数组(克隆)

数组是引用数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组

const a1 = [1, 2];
const a2 = a1;
​
a2[0] = 2;
a1 // [2, 2]
// 修改a2同时也修改了a1,因为他们指向同一个地址,也就是同一个数组

克隆数组

// ES5 只能用变通方法来复制数组。
const a1 = [1, 2];
const a2 = a1.concat();
​
a2[0] = 2;
a1 // [1, 2]
// 上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。
// 扩展运算符提供了复制数组的简便写法。
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

上面的两种写法,a2都是a1的克隆。

2. 合并数组

扩展运算符提供了数组合并的新写法。

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
​
// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
​
// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
​
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
​
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
​
a3[0] === a1[0] // true
a4[0] === a1[0] // true

不过,这两种方法都是浅拷贝,使用的时候需要注意,它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。

3. 与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组。

// ES5
a = list[0]
rest = list.slice(1)
// ES6
[a, ...rest] = list
​
// 下面是另外一些例子
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]
​
const [first, ...rest] = [];
first // undefined
rest  // []
​
const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
​
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

4. 字符串

扩展运算符还可以将字符串转为真正的数组。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

涉及到操作四个字节的 Unicode 字符的函数,最好都用扩展运算符改写。

5. 实现了Iterator接口对象
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

上面代码中,querySelectorAll方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。

Number.prototype[Symbol.iterator] = function*() {
  let i = 0;
  let num = this.valueOf();
  while (i < num) {
    yield i++;
  }
}
​
console.log([...5]) // [0, 1, 2, 3, 4]

上面代码中,先定义了Number对象的遍历器接口,扩展运算符将5自动转成Number实例以后,就会调用这个接口,就会返回自定义的结果。

6. Map 和 Set结构,Generator函数

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
​
let arr = [...map.keys()]; // [1, 2, 3]

Array.from()

Array.from()方法用于将两类对象转为真正的数组

一个类似数组的对象,Array.from将它转为真正的数组。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};
​
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
​
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。

// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  return p.textContent.length > 100;
});
​
// arguments对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

上面代码中,querySelectorAll方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter方法。 Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
​
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

Array.of()

Array.of()方法用于将一组值,转换为数组。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

copyWithin()

数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组

Array.prototype.copyWithin(target, start = 0, end = this.length)
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。

它接受三个参数。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

这三个参数都应该是数值,如果不是,会自动转为数值。

find() 和 findIndex()

find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

[1, 4, -5, 10].find((n) => n < 0)
// -5// 接受三个参数,依次为当前的值、当前的位置和原数组。
[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

fill()

fill方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]
​
new Array(3).fill(7)
// [7, 7, 7]

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

数组扁平化flat(), flatMap()

Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

上面代码中,原数组的成员里面有一个数组,flat()方法将子数组的成员取出来,添加在原来的位置。

flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]][1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

上面代码中,flat()的参数为2,表示要“拉平”两层的嵌套数组。

如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]

如果原数组有空位,flat()方法会跳过空位。

2. 对象扩展

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}// 等同于
const baz = {foo: foo};
​
function f(x, y) {
  return {x, y};
}
​
// 等同于function f(x, y) {
  return {x: x, y: y};
}
​
f(1, 2) // Object {x: 1, y: 2}
let birth = '2000/01/01';
​
const Person = {
​
  name: '张三',
​
  //等同于birth: birth
  birth,
​
  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }
​
};

CommonJS 模块输出一组变量,就非常合适使用简洁写法。

let ms = {};
​
function getItem (key) {
  return key in ms ? ms[key] : null;
}
​
function setItem (key, value) {
  ms[key] = value;
}
​
function clear () {
  ms = {};
}
​
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

let propKey = 'foo';
​
let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
  ['h' + 'ello']() {
    return 'hi';
  }
};
​
obj.hello() // hi

可枚举型

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

四个操作会忽略enumerablefalse的属性

(1)for...in

for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

(2)Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

(5)Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。

super关键字

this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

const proto = {
  foo: 'hello'
};
​
const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};
​
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代码中,对象obj.find()方法之中,通过super.foo引用了原型对象protofoo属性。 注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

对象扩展符

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

扩展运算符等同于使用Object.assign()方法。

解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误

解构赋值必须是最后一个参数,否则会报错。

let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误

Object.is()

它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

+0 === -0 //true
NaN === NaN // falseObject.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.getOwnpropertyDescriptors()

ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

Object.getOwnPropertyDescriptors()方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

Object.assign()

Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

const target = { a: 1 };
​
const source1 = { b: 2 };
const source2 = { c: 3 };
​
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。

注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

第三十四天

1. 运算符扩展

1.1 指数运算符—— **

2 ** 2 //  2^2 4
2 ** 3 // 2^3 8

指数运算符可以与等号结合,形成一个新的赋值运算符(**=

let a = 1.5;
a **= 2;
// 等同于 a = a * a;
​
let b = 4;
b **= 3;
// 等同于 b = b * b * b;

1.2 链判断运算符—— ?.

如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。&&

// 错误的写法
const  firstName = message.body.user.firstName || 'default';
​
// 正确的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value;

直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined

a?.[++x]
// 等同于
a == null ? undefined : a[++x]
// 判断对象方法是否存在,如果存在就立即执行的例子。
iterator.return?.()

链判断运算符?.有三种写法。

  • obj?.prop // 对象属性是否存在
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法是否存在

1.3 Null判断运算符——??

读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。常见做法是通过 ||运算符指定默认值属性的值如果为空字符串或false0,默认值也会生效

为了避免这种情况,引入Null 判断运算符?? 。它的行为类似||,但是**只有运算符左侧的值为nullundefined**时,才会返回右侧的值。

const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

上面代码中,默认值只有在左侧属性值为nullundefined时,才会生效。

跟链判断运算符?.配合使用,为nullundefined的值设置默认值。

const animationDuration = response.settings?.animationDuration ?? 300;

1.4 逻辑赋值运算符

// 或赋值运算符
x ||= y
// 等同于
x || (x = y)
​
// 与赋值运算符
x &&= y
// 等同于
x && (x = y)
​
// Null 赋值运算符
x ??= y
// 等同于
x ?? (x = y)

这三个运算符||=&&=??=相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。

它们的一个用途是,为变量或属性设置默认值。

// 老的写法
user.id = user.id || 1;
​
// 新的写法
user.id ||= 1;

2. Symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol()写法没有登记机制,每次调用都会返回一个不同的值

let s = Symbol();
​
typeof s
// "symbol"
// 表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。

Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

Symbol.prototype.description

提供了一个实例属性description,直接返回 Symbol 的描述。

const sym = Symbol('foo');
​
sym.description // "foo"

作为属性名的Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。

let mySymbol = Symbol();
​
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
​
// 第二种写法
let a = {
  [mySymbol]: 'Hello!',
  mySymbol: '11'
};
​
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
​
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

image-20211116185718004

Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 Symbol属性不会出现for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');
​
obj[a] = 'Hello';
obj[b] = 'World';
​
const objectSymbols = Object.getOwnPropertySymbols(obj);
​
objectSymbols
// [Symbol(a), Symbol(b)]

Symbol.for(), Symbol.keyFor()

重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局

Symbol.for("bar") === Symbol.for("bar")
// trueSymbol("bar") === Symbol("bar")
// false

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined

3. Set 和 Map 数据结构

Set

基本用法: 它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

const s = new Set();[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
​
for (let i of s) {
  console.log(i);
}
// 2 3 5 4

通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

Set实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 结构的实例有四个遍历方法,可以用于遍历成员。

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。

  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。

  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。

  • Set.prototype.clear():清除所有成员,没有返回值。

    s.add(1).add(2).add(2);
    // 注意2被加入了两次
    ​
    s.size // 2
    ​
    s.has(1) // true
    s.has(2) // true
    s.has(3) // false
    ​
    s.delete(2);
    s.has(2) // false
    

Array.from方法可以将 Set 结构转为数组。

// 提供了去除数组重复成员的另一种方法。
function dedupe(array) {
  return Array.from(new Set(array));
}
​
dedupe([1, 1, 2, 3]) // [1, 2, 3]

Map

基本用法: 本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

const data = {};
const element = document.getElementById('myDiv');
​
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"

它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

Map实例的属性和方法

属性和操作

  • size属性返回 Map 结构的成员总数。
  • set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。(可以链式使用
  • get方法读取key对应的键值,如果找不到key,返回undefined
  • has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • delete方法删除某个键,返回true。如果删除失败,返回false
  • clear方法清除所有成员,没有返回值。
const m = new Map();
m.set('foo', true).set('mine', 123);
m.set('edition', 6)        // 键是字符串
m.size // 3
​
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello)  // Hello ES6!
​
m.has(hello) // true
​
m.delete(hello)
m.has(hello)       // false
​
m.clear()
m.size // 0

遍历方法:需要特别注意的是,Map 的遍历顺序就是插入顺序。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

Map 转为数组: 比较快速的方法是使用扩展运算符(...)。

Map 转为 JSON: JSON.stringify(strMapToObj(strMap));

JSON 转为 Map: objToStrMap(JSON.parse(jsonStr));

对象转为 Map: 可以通过Object.entries()

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

Map 转为对象: 如果所有 Map 的键都是字符串,它可以无损地转为对象

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}
​
const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

第三十五天

1. Promise 对象

Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

特点:

  • 对象状态不受外界影响

    Promise代表一种异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败);只有异步操作决定当前状态。

    缺点:

    • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
    • 不设置回调函数,Promise内部抛出的错误,不会反应到外部
    • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚开始或即将结束)
  • 一旦状态改变,就不会在变

    状态的变化只有两种可能,pending ——> fulfilledpending ——> rejected,变化之后,任何操作都不能改变其状态,这个时候称为 resolved(已定型)

基本用法

Promise对象是一个构造函数,用来生成Promise实例。

构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

const promise = new Promise(function(resolve, reject) {
  // ... some code
​
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

resolve函数的作用:状态从“未完成”变为“成功”(即从 pending 变为 fulfilled)

reject函数的作用:状态从“未完成”变为“失败”(即从 pending 变为 rejected)

Promise.prototype.then()

调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。

then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

注意: 在then函数里面写上return 才能到下一个then函数里面

//  利用箭头函数
getJSON("/posts.json")
.then(json => return json.post)
.then(post => {
  // ...
});

Promise.prototype.catch()

捕捉执行中出错的函数: Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

getJSON('/posts.json').then(posts => {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

// finally 不接受任何参数;即finally执行与状态无关
promise.then(result => {···})
    .catch(error => {···})
    .finally(() => {···});

不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

在成功或失败都需要执行的语句可以写在finally函数里面

Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});
​
Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);
​
p
.then(res => console.log(res))
.catch(err => console.log(err));

只要p1p2p3中有一个实例率先改变状态p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数

Promise.allSettled()

一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作。但是,现有的 Promise 方法很难实现这个要求。

Promise.all()方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。

Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)。Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

const promises = [
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];
​
await Promise.allSettled(promises);
removeLoadingIndicator();

Promise.any()

该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

只要参数实例有一个变成fulfilled状态包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态包装实例就会变成rejected状态

Promise.resolve()

将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
​
p.then(null, function (s) {
  console.log(s)
});
// 出错了

2. Aysnc await

基本用法: async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}
​
getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

async

async函数的语法规则总体上比较简单,难点是错误处理机制。 async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

async function f() {
  return 'hello world';
}
​
f().then(v => console.log(v))
// "hello world"

await

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async function f() {
  // 等同于
  // return 123;
  return await 123;
}
​
f().then(v => console.log(v))
// 123

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

async function f() {
  await Promise.reject('出错了');
}
​
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了

如果有多个await命令,可以统一放在try...catch结构中。

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);
​
    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }

3. Proxy——拦截

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);
var today = {}
var proxy = new Proxy(today, {
  get: function(target, propKey) {
    return 35;
  }
});
​
today.time // 35
today.name // 35
today.title // 35

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },
​
  apply: function(target, thisBinding, args) {
    return args[0];
  },
​
  construct: function(target, args) {
    return {value: args[1]};
  }
};
​
var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);
​
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

4. Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}
​
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

// 老写法
'assign' in Object // true// 新写法
Reflect.has(Object, 'assign') // true

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});

上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。

第三十六天

1. 遍历器

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:

一是为各种数据结构,提供一个统一的、简便的访问接口;

二是使得数据结构的成员能够按某种次序排列;

三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

Iterator 的遍历过程:

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

示例:输出10以内的数字之和

var obj = {
            [Symbol.iterator]() {
                var a = 0;
                var b = 1;
                let n = 0;
                return {
                    next: function () {
                        let result = b;
                        b = a + b;
                        a = result;
                        n++;
                        if (n >= 10) {
                            return { value: undefined, done: true }
                        }
                        return { value: result, done: false }
                    }
                }
            }
        }

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

return(),throw()

除了具有next()方法,还可以具有return()方法和throw()方法。

return()方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法

如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()方法。

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
}

2. 生成器Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数是一个普通函数,但是有两个特征。

一是,function关键字与函数名之间有一个星号;

二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
​
var hw = helloWorldGenerator();

该函数有三个状态:hello,world 和 return 语句(结束执行)。

调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

yield表达式

Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

3. class

传统方法写构造函数

function Point(x, y) {
  this.x = x;
  this.y = y;
}
​
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};
​
const p = new Point(1, 2);
p.toString(); // (1, 2)

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。(它的绝大部分功能,ES5 都可以做到)

class Point {
  //  如果没有显式定义,一个空的constructor()方法会被默认添加。
  // class Point {
  // }
  // 等同于
  // class Point {
  //  constructor() {}
  // }
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
​
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
// ES6 的类,完全可以看作构造函数的另一种写法。
Point === Point.prototype.constructor // true// 使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
// 类必须使用new调用,否则会报错
const p = new Point(1, 2);
p.toString(); // (1, 2)

类的实例

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

var point = new Point(2, 3);
​
point.toString() // (2, 3)
​
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

toString()是原型对象的属性(因为定义在Point类上),所以hasOwnProperty()方法返回false。这些都与 ES5 的行为保持一致。

类的所有实例共享一个原型对象。

var p1 = new Point(2,3);
var p2 = new Point(3,2);p1.__proto__ === p2.__proto__
//true

上面代码中,p1p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。

这也意味着,可以通过实例的__proto__属性为“类”添加方法。

取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}
​
let inst = new MyClass();
​
inst.prop = 123;
// setter: 123
​
inst.prop
// 'getter'

静态方法

如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
​
Foo.classMethod() // 'hello'var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

父类的静态方法,可以被子类继承;静态方法也是可以从super对象上调用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
​
class Bar extends Foo {
    fn() {
        return super.classMethod();
    }
}
​
Bar.classMethod() // 'hello'
Bar.fn() // 'hello'

注意点

  1. 严格模式

    类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

  2. 不存在提升

    类不存在变量提升(hoist),这一点与 ES5 完全不同。

  3. name 属性

    由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。

    class Point {}
    Point.name // "Point"
    
  4. Generator 方法

    如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

  5. this 的指向

    类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

    2021.11.8

1. class的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {
    constructor(x, y) {
    this.x = x;
    this.y = y;
  }
​
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
​
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
​
  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}
​
let cp = new ColorPoint(25, 8, 'green');
​
cp instanceof ColorPoint // true
cp instanceof Point // true

定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

super()在这里相当于Point.prototype.constructor.call(this)

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

super

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}
​
class B extends A {
  m() {
    super(); // 报错
  }
}

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  p() {
    return 2;
  }
}
​
class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}
​
let b = new B();

2. 模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

// CommonJS模块
let { stat, exists, readfile } = require('fs');
​
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

注意:ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

export命令

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
export function multiply(x, y) {
  return x * y;
};

export的写法,除了像上面这样,还有另外一种。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
function v1() { ... }
function v2() { ... }
​
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
export { firstName, lastName, year };

使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500 毫秒之后变成baz

import命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

// main.js
import { firstName, lastName, year } from './profile.js';
​
function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

import {a} from './xxx.js'
​
a = {}; // Syntax Error : 'a' is read-only;

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'my_module';
​
// 报错
let module = 'my_module';
import { foo } from module;
​
// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。

整体加载

// circle.jsexport function area(radius) {
  return Math.PI * radius * radius;
}
​
export function circumference(radius) {
  return 2 * Math.PI * radius;
}

逐一加载这个模块。

// main.jsimport { area, circumference } from './circle';
​
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

整体加载

import * as circle from './circle';
​
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

import * as circle from './circle';
​
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};

export default命令

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}
​
// export-default.js
export default function foo() {
  console.log('foo');
}
​
// 或者写成function foo() {
  console.log('foo');
}
​
export default foo;

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'

因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

// 正确
export default 42;
​
// 报错
export 42;

转发 export和import复合写法

在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';
​
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

// 接口改名
export { foo as myFoo } from 'my_module';
​
// 整体输出
export * from 'my_module';
​
export * as ns from "mod";// 等同于import * as ns from "mod";export {ns};

第三十七天

jQuery

jQuery是一个快速、简洁的JavaScript库,其设计的宗旨是"write Less, Do More",即倡导写更少的代码,做更多的事情

优点

  • 轻量级,核心文件才几十kb,不会影响页面加载速度
  • 跨浏览器兼容,基本兼容了现在主流的浏览器
  • 链式编程,隐式迭代
  • 对事件、样式、动画支持,大大简化了DOM操作
  • 支持插件扩展开发,有着丰富的第三方插件,例如:树形菜单、日期控件、轮播图等
  • 免费、开源

使用

// 引入jQuery文件
<script src="..." ></script>
// jQuery的入口函数
$(function () {
    ...// 此处是页面DOM加载完成的入口
});
$(document).ready(function() {
    ... // 此处是页面DOM加载完成的入口
});

等着DOM结构渲染完毕即可执行内部代码,不必等到所有外部资源加载完成,jQuery帮我们完成了封装(相当于原生js中的DOMContentLoaded,不同的是原生js中的load事件是等页面文档、外部js文件、css文件、图片加载完毕才执行内部代码)

jQuery常用的API

  • $("选择器") 里面选择器直接写CSS选择器即可,但是要加引号
名称用法描述
ID选择器$("#id")获取指定ID的元素
全选选择器$("*")匹配所有元素
类选择器$(".class")获取同一类的class元素
标签选择器$("div")获取同一类标签的所有元素
并集选择器$("div,p,li")选取多个元素
交集选择器$("li.current")交集元素
子代选择器$("ul>li")使用>号,获取亲儿子层级的元素;并不会获取孙子层级的元素
后代选择器$("ul li")使用空格,代表后代选择器,获取ul下的所有li元素,包括孙子等
  • jQuery筛选选择器
语法用法描述
:first$("li:first")获取第一个li元素
:last$("li:last")获取最后一个li元素
:eq(index)$("li:eq(2)")获取到的li元素中,选择索引号为2的元素(索引号index从0开始)
:odd$("li:odd")获取到的li元素中,选择索引号为奇数的元素
:even$("li:even")获取到的li元素中,选择索引号为偶数的元素
  • jQuery筛选方法
语法用法说明
parent()$("li").parent()查找父级
children(selector)$("ul").children("li")相当于$("ul>li"),最近一级(亲儿子)
find(selector)$("ul").find("li")相当于$("ul li"),后代选择器
siblings(selector)$(".first").siblings("li")查找兄弟节点,不包括自己本身
nextAll([expr])$(".first").nextAll()查找当前元素的所有弟弟元素
prevAll([expr])$(".last").prevAll()查找当前元素的所有哥哥元素
hasClass(class)$('div').hasClass("protected")检查当前元素是否含有某个特定的类,如果有,则返回true
eq(index)$("li").eq(2)相当于$("li:eq(2)"),index从0开始

jQuery里面的排他思想

想要多选一的效果,排他思想,当前元素设置样式,其余的兄弟清除样式

siblings方法

<button>第1个按钮</button>
<button>第2个按钮</button>
<button>第3个按钮</button>
<button>第4个按钮</button>
<button>第5个按钮</button>
<script>
    $(function() {
        // 隐式迭代 给所有的按钮绑定点击事件
        $("button").click(function() {
            // 当前的元素变化背景颜色
            $(this).css("background", "blue");
            // 其余的兄弟去掉背景颜色  隐式迭代
            $(this).siblings("button").css("background", "");
        });
    })
</script>

jQuery样式操作

操作样式之CSS方法-

  • $(this).css("color"); 参数只写属性名,则返回属性值
    • $(this).css("color", "red"); 参数是属性名,属性值,用逗号分隔,是设置一组样式,属性必须加引号,值如果是数字可以不用跟单位和引号
    • $(this).css({"color":"black","font-size":"20px"})参数可以是对象形式,方便设置多组样式,属性名和属性值用冒号隔开,属性可以不用加引号($(this).css({color:"black", font-size:20})),如果是复合属性则必须采用驼峰命名法,如果属性值不是数字,则需要加引号

设置类样式方法

    • 作用等同于以前的classList,可以操作类样式,注意操作类里面的参数不要加点
    • 添加类 $("div").addClass("类名");
    • 删除类 $("div").removeClass("类名");
    • 切换类 $("div").toggleClass("类名");
    • 类操作和className的区别:原生JS中的className会覆盖元素原先里面的类名;jQuery里面类操作只是对指定类进行操作,不影响原先的类名

jQuery动画效果

jQuery给我们封装了很多的动画效果,最为常见的如下

  • 显示隐藏:show()hide()toggle()
  • 滑动:slideDown()slideUp()slideToggle()
  • 淡入淡出:fadeIn()fadeOut()fadeToggle()fadeTo()
  • 自定义动画:animate()

动画队列及其停止排队方法

  • 动画或效果队列
  • 动画或者效果一旦触发就会执行,如果多次触发,就造成多个动画或者效果排队执行
  • 停止排队 stop()
    • stop()方法用于停止动画或效果
    • 注意:stop()写到动画或者效果的前面,相当于停止结束上一次的动画

jQuery事件

  • 单个事件注册
    • 语法:element.事件(function(){});例如:$("div").click(function(){事件处理程序}),其他事件与原生基本一致
    • 比如:mouseovermouseoutblurfocuschangekeydownkeyupresizescroll

事件绑定

  • 事件处理 on() 绑定事件
    • on()方法在匹配元素上绑定一个或多个事件的事件处理函数
    • 语法规范:

element.on(events,[selector],fn)

      • events:一个或多个用空格分隔的事件类型,如:clickkeydown
      • selector:元素的子元素选择器
      • fn:回调函数 即绑定在元素身上的侦听函数

on()方法的优势: 可以绑定多个事件,多个事件处理程序

<!-- 此处引入jQuery文件,你就当这个文件存在吧 -->
<script src="jquery.min.js"></script>
<script>
    $(function() {
        // 单个事件注册
        $("div").click(function() {
            $(this).css("background", "blue");
        });
        $("div").mouseenter(function() {
            $(this).css("background", "red");
        });
​
        // 事件处理on()
        $("div").on({
            click: function() {
                $(this).css("background", "blue");
            },
            mouseenter: function() {
                $(this).css("background", "red");
            }
            mouseleave: function() {
                $(this).css("background", "purple");
            }
        });
    })
</script>

off()方法可以移除通过on()方法添加的事件处理程序、

$("p").off(); // 解绑p标签所有事件处理程序
$("p").off("click"); // 解绑p标签元素上面的点击事件
$("ul").off("click", "li"); // 解绑事件委托

如果有的事件只想触发一次,可以用one()来绑定事件

$("p").one("click", function() {
    alert(57); // 只触发一次
})

jQuery里面的一些常用方法

如果想要把某个对象拷贝(合并)给另外一个对象使用,此时可以用$.extend()方法

  • 语法

    • deep:如果设为true为深拷贝,默认为false浅拷贝
    • target:要拷贝的目标对象
    • object1:待拷贝到第一个对象的对象

$.extend([deep], target, object1, [objectN])

let targetObj = {
        id: 0
    };
    let obj = {
        id: 1,
        name: "andy"
        byc: {
            age: 18
        }
    };
    $.extend(targetObj, obj);
  • 浅拷贝是把被拷贝的对象复杂数据类型中的地址拷贝给目标对象,修改目标对象会影响被拷贝对象
  • 深拷贝,前面加true,完全克隆(拷贝的是对象,不是地址),修改目标对象不会影响被拷贝对象