前端从零开始第七周

295 阅读9分钟

第三十天

草方格官网实现(swiper轮播图插件,gitee上传网站)

szcodeman.gitee.io/daily-demo/…

网页答题小程序

手机端:szcodeman.gitee.io/daily-demo/…

第三十一天

ES6 与 ECMAScript 2015 的关系

ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

Babel 转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。

// 转码前
input.map(item => item + 1);
​
// 转码后
input.map(function (item) {
  return item + 1;
});

上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。

下面的命令在项目目录中,安装 Babel。

$ npm install --save-dev @babel/core

1. let 和 const

let 和 const 都是和 var 一样,用来声明变量的;

// const声明一个只读的常量。一旦声明,常量的值就不能改变。
// const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
const PI = 3.1415;PI // 3.1415
PI = 3;// TypeError: Assignment to constant variable.

let 和 var区别

for循环的计数器,就很合适使用let命令。

for (let i = 0; i < 10; i++) {
  // ...
}
​
console.log(i);
// ReferenceError: i is not defined
// let 声明变量是块级作用域里面的i,在外引用就是未定义,for循环体中每个i都是独立在各自作用域中的i
​
for (var i = 0; i < 10; i++) {
  // ...
}
​
console.log(i);
// 10
// var 声明的变量是全局作用域中的i,每次应用改变的都是全局的i,所以for循环中的i都是同一个i
不存在变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined

为了纠正这种现象(变量提升),let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
​
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;
​
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

2. globalThis 对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this关键字,但是有局限性。

ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this

垫片库global-this模拟了这个提案,可以在所有环境拿到globalThis

3. 变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构

3.1 数据的解构赋值

以前,为变量赋值,只能直接指定值。

let a = 1;
let b = 2;
let c = 3;
let [foo] = [];

ES6 允许写成下面这样;如果解构不成功,变量的值就等于undefined。

let [a, b, c] = [1, 2, 3];
// a: 1, b: 2, c: 3
let [bar, foo] = [1];
// foo: undefined
let [q, [w], e] = [1, [2, 3], 4];
// q: 1, w: 2, e: 4 

如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

// 下面都会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
默认值

解构赋值允许指定默认值。

let [foo = true] = [];foo 
// truelet [x, y = 'b'] = ['a']; 
// x='a', y='b' => let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

默认值可以引用解构赋值的其他变量,但该变量必须已经声明

let [x = 1, y = x] = [];     
// x=1; y=1 let [x = 1, y = x] = [2];    
// x=2; y=2 let [x = 1, y = x] = [1, 2]; 
// x=1; y=2 let [x = y, y = 1] = [];    
// ReferenceError: y is not defined

3.2 对象的解构赋值

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };foo // "aaa"bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,

变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值

属性名不对或不存在该属性的话,取不到值

let {bar: bar, foo: foo} = { foo: 'aaa', bar: 'bbb' };
// 下面是简写,冒号后面是变量,变量和属性名项目可以简易写
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
let {foo} = {bar: 'baz'};foo // undefined

解构也可以用于嵌套结构的对象。

let obj = {  p: [    'Hello',    { y: 'World' }  ]};
let { p: [x, { y }] } = obj;x // "Hello"y // "World"
//注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。
let obj = {  p: [    'Hello',    { y: 'World' }  ]};
let { p, p: [x, { y }] } = obj;x // "Hello"y
​
const node = {  loc: {    start: {      line: 1,      column: 5    }  }};
let { loc, loc: { start }, loc: { start: { line }} } = node;line 
// 1loc  // Object {start: Object}start 
// Object {line: 1, column: 5}
// 只有line才是变量, 其他两个都是模式

默认值

对象的解构也可以指定默认值。

var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

默认值生效的条件是,对象的属性值严格等于undefined

var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null

上面代码中,属性x等于null,因为nullundefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。

3.3 解构赋值规则

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeErrorlet { prop: y } = null; // TypeError
let {toString: s} = 123;
s === Number.prototype.toString 
// true
let {toString: s} = true;
s === Boolean.prototype.toString // true

3.4 函数参数的解构赋值

函数的参数也可以使用解构赋值。

function add([x, y]){  return x + y;}
add([1, 2]); // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy。对于函数内部的代码来说,它们能感受到的参数就是xy

函数参数的解构也可以使用默认值。

function move({x = 0, y = 0} = {}) {  
    return [x, y];
}
​
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

函数move的参数是一个对象,通过对这个对象进行解构,得到变量xy的值。如果解构失败,xy等于默认值。

3.5 圆括号问题

以下三种解构赋值不得使用圆括号

  1. 变量声明语句

    // 全部报错
    let [(a)] = [1];
    let {x: (c)} = {}; 
    let ({x: c}) = {}; 
    let {(x: c)} = {}; 
    let {(x): c} = {}; 
    let { o: ({ p: p }) } = { o: { p: 2 } };
    
  2. 函数参数

    函数参数也属于变量声明,因此不能带有圆括号。

    // 报错
    function f([(z)]) { return z; }
    // 报错
    function f([z,(x)]) { return x; }
    
  3. 赋值语句的模式

    // 全部报错({ p: a }) = { p: 42 };([a]) = [5];
    
    // 报错[({ p: a }), { x: c }] = [{}, {}];
    

可以使用圆括号的情况

可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

3.4 解构赋值的应用

  1. 交换变量的值

    let x = 1;let y = 2;[x, y] = [y, x];
    
  2. 函数返回多个值

    // 返回一个数组
    function example() {  
        return [1, 2, 3];
    }
    let [a, b, c] = example();
    // 返回一个对象
    function example() {  
        return {    
            foo: 1,    
            bar: 2  
        };
    }
    let { foo, bar } = example();
    
  3. 函数参数的定义

    // 参数是一组有次序的值
    function f([x, y, z]) {  ... }
    f([1, 2, 3]);
    // 参数是一组无次序的值
    function f({x, y, z}) { ... }
    f({z: 3, y: 2, x: 1});
    
  4. 提取JSON数据

    let jsonData = {  
        id: 42,  
        status: "OK",  
        data: [867, 5309]
    };
    let { id, status, data: number } = jsonData;
    console.log(id, status, number);// 42, "OK", [867, 5309]
    
  5. 函数参数的默认值

    jQuery.ajax = function (
        url, {  
        async = true,  
        beforeSend = function () {},  
        cache = true,  
        complete = function () {},  
        crossDomain = false,  
        global = true, 
        // ... more config} = {}
        ) {  
        // ... do stuff
    };
    
  6. 遍历Map结构

    const map = new Map();
    map.set('first', 'hello');
    map.set('second', 'world');
    for (let [key, value] of map) {  console.log(key + " is " + value);}
    // first is hello
    // second is world
    
  7. 输入模块的指定方法

    import { Input, Button, List } from "antd"
    

第三十二天

1. 字符串扩展

1.1 字符的Unicode表示法

ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

"\u0061"
// "a"

这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。 有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。

'\z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

1.2 字符串的遍历

ES6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历。

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

let text = String.fromCodePoint(0x20BB7);
​
for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "for (let i of text) {
  console.log(i);
}
// "𠮷"

1.3 模板字符串

传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

表示多行字符串,所有的空格和缩进都会被保留在输出之中

模板字符串中嵌入变量,需要将变量名写在${}之中。

1.4 标签(函数)模板

模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能。

alert`hello`
// 等同于
alert(['hello'])

如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。模板字符串前面的标签其实就是函数名

let a = 5;
let b = 10;
​
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

1.5 字符串新增方法

1.5.1 String.raw()

ES6 为原生的 String 对象,提供了一个raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。

String.raw`Hi\n${2+3}!`
// 实际返回 "Hi\n5!",显示的是转义后的结果 "Hi\n5!"String.raw`Hi\u000A!`;
// 实际返回 "Hi\u000A!",显示的是转义后的结果 "Hi\u000A!"

如果原字符串的斜杠已经转义,那么String.raw()会进行再次转义。

1.5.2 includes(), startsWith(), endsWith()

传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes() :返回布尔值,表示是否找到了参数字符串。
  • startsWith() :返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith() :返回布尔值,表示参数字符串是否在原字符串的尾部
1.5.3 repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被取整。

'na'.repeat(2.9) // "nana"

如果repeat的参数是负数或者Infinity,会报错。参数NaN等同于 0。

如果repeat的参数是字符串,则会先转换成数字

1.5.4 trimStart(),trimEnd()

ES2019 对字符串实例新增了trimStart()trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '  abc  ';
​
s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

上面代码中,trimStart()只消除头部的空格,保留尾部的空格。trimEnd()也是类似行为。

1.5.5 replaceAll()

字符串的实例方法replace()只能替换第一个匹配。

'aabbcc'.replace('b', '_')// 'aa_bcc''aabbcc'.replaceAll('b', '_')// 'aa__cc'

2. 正则扩展

2.1 RegExp构造函数

第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

var regex = new RegExp(/xyz/i);
// 等价于
var regex1 = /xyz/i;
console.log(regex == regex1); // false

3. 数值扩展

3.1 二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

0b111110111 === 503 // true
0o767 === 503 // true

如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。

Number('0b111')  // 7
Number('0o10')  // 8

3.2 数值分隔符

ES2021,允许 JavaScript 的数值使用下划线(_)作为分隔符。

let budget = 1_000_000_000_000;
budget === 10 ** 12 // true

这个数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。

123_00 === 12_300 // true12345_00 === 123_4500 // true
12345_00 === 1_234_500 // true

小数和科学计数法也可以使用数值分隔符。

// 小数
0.000_001
​
// 科学计数法
1e10_000

数值分隔符有几个使用注意点。

  • 不能放在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的eE前后不能有分隔符。

3.3 Number.isFinite(), Number.isNaN()

ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。

Number.isFinite() 用来检查一个数值是否为有限的(finite),即不是Infinity。

Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

Number.isNaN() 用来检查一个值是否为NaN

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true

如果参数类型不是NaNNumber.isNaN一律返回false

3.4 Math对象的扩展

ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

3.4.1 Math.trunc()

Math.trunc方法用于去除一个数的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

对于非数值,Math.trunc内部使用Number方法将其先转为数值。

对于空值和无法截取整数的值,返回NaN

3.4.2 Math.sign()

Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。它会返回五种值。

  • 参数为正数,返回+1
  • 参数为负数,返回-1
  • 参数为 0,返回0
  • 参数为-0,返回-0;
  • 其他值,返回NaN
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN

如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN

3.4.3 Math.hypot()

Math.hypot方法返回所有参数的平方和的平方根。

Math.hypot(3, 4);        // 5 勾股定理

4. BigInt数据类型

// 超过 53 个二进制位的数值,无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true// 超过 2 的 1024 次方的数值,无法表示
Math.pow(2, 1024) // Infinity

ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题,这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

const a = 2172141653n;
const b = 15346349309n;
​
// BigInt 可以保持精度
a * b // 33334444555566667777n// 普通整数无法保持精度
Number(a) * Number(b) // 33334444555566670000

为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n

1234 // 普通整数
1234n // BigInt// BigInt 的运算
1n + 2n // 3n

BigInt 同样可以使用各种进制表示,都要加上后缀n

BigInt 与普通整数是两种值,它们之间并不相等。

42n === 42 // false

typeof运算符对于 BigInt 类型的数据返回bigint

typeof 123n // 'bigint'

4. 函数扩展

4.1 参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

// 例一
function f(x = 1, y) {
  return [x, y];
}
​
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}
​
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]// 推荐
function f(x, z, y=5) {
    return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5,undefined]
f(1, 2) // [1, 5, 2]
f(1, 2, 3) // [1, 3, 2]

上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined

4.2 函数length属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
​
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

4.3 作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

var x = 1;
​
function f(x, y = x) {
  console.log(y);
}
​
f(2) // 2
​
let x = 1;
​
function f(y = x) {
  let x = 2;
  console.log(y);
}
​
f() // 1

4.4 rest 参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {  
  let sum = 0;  
  for (var val of values) 
  {    
    sum += val;  
  }  
  return sum;
}
add(2, 5, 3) // 10

add函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。

4.5 箭头函数

基本用法

ES6 允许使用“箭头”(=>)定义函数。

var f = v => v;
// 等同于
var f = function (v) {  return v;};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {  return num1 + num2;};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

下面是一种特殊情况,虽然可以运行,但会得到错误的结果。

let foo = () => { a: 1 };foo() // undefined