前端工作室专注前端开发
 博客首页

《ES6标准入门》(第3版作者:阮一峰)读书笔记 1-9章

2021-03-16 09:43:22
JS基础与深入
55
文章图片

对国内十分有名的一本书--《ES6标准入门》进行个人的读书总结,对书中的知识点进行实践验证和记录。

《ES6标准入门》(第3版作者:阮一峰)读书笔记 1-9章

ES6发布以来收到大家的一致追捧,本篇即对国内十分有名的一本书--《ES6标准入门》进行个人的读书总结,对书中的知识点进行实践验证和记录。想要看原版的请购买正版书籍。

第1章 ECMAScript 简介

1.6 Babel 转码器

Babel 转码器可以将ES6转换为ES5,所以可以放心使用ES6进行编写。

1.6.1 配置文件.babelrc

我们新建一个文件夹,然后在文件夹里面新建.babelrc文件,该文件可以设置转码规则和插件。基本格式如下:

{
  "presets": [] ,
  "plugins":[]
}

presets字段设定转码规则,例如:

#最新转码规则
$ npm install --save -dev babel-preset-latest
# react 转码规则
$ npm install --save-dev babel-preset-react
#不同阶段语法提案的转码规则(共有 个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3

我们先安装最新转码规则,然后将最新转码规则添加到.babelrc文件中

{
  "presets": [
    "latest"
  ] ,
  "plugins":[]
}

顺便说一下安装命令的区别

npm install X –save:会把X包安装到node_modules目录中会在package.json的dependencies属性下添加X之后运行npm install命令时,会自动安装X到node_modules目录中之后运行npm install –production或者注明NODE_ENV变量值为production时,会自动安装msbuild到node_modules目录中 

npm install X –save-dev:会把X包安装到node_modules目录中会在package.json的devDependencies属性下添加X之后运行npm install命令时,会自动安装X到node_modules目录中之后运行npm install –production或者注明NODE_ENV变量值为production时,不会自动安装X到node_modules目录中

命令行转码babel-cli

Babel 提供 babel-cli 工具,用于命令行转码。 首先安装它

$ npm install --global babel-cli // 注意必须全局安装才能使用babel命令

执行下面命令来输出结果

$ babel example.js

如果只是在项目里安装,我就采用了这种方式

$ npm install babel-cli //会把包安装到node_modules目录中不会修改package.json之后运行npm install命令时,不会自动安装

执行下面命令来输出转码结果

$ npx babel example.js

报错信息:

example.js doesn't exist

我们创建该example.js文件,并写下以下内容,再执行一遍npx babel example.js。

'use strict'
let res = [1,2,3,4,5,6].find(item=>{
  return item > 4
})
console.log(res)
let arr = [1,2,3,4,5,6].map(item=>{
  return item*2
})
console.log(arr)

输出的转码结果

var res = [1, 2, 3, 4, 5, 6].find(function (item) {
  return item > 4;
});
console.log(res);
var arr = [1, 2, 3, 4, 5, 6].map(function (item) {
  return item * 2;
});
console.log(arr);

现在我们可以看到输出结果了,但是我们肯定是想要把结果写进去某个文件

#一out file 参数指定输出文件
$ npx babel example.js --out-file compiled.js  // 全局安装使用:babel example.js --out-file compiled.js
#或者
$ npx babel example.js -o compiled.js // 全局安装使用:babel example.js -o compiled.js

而且会覆盖文件原有的内容,文件不存在时会自动创建。 如果需要整个目录转码,我们先新建一个src文件来测试以下,把example.js文件丢进去

# --out-dir 或- 参数指定输出目录
$ npx babel src --out-dir lib // 全局安装使用:babel src --out-dir lib
#或者
$ npx babel src -d lib // 全局安装使用:babel src -d lib

转码结果被放到lib文件夹下,同为example.js。

生成source map文件

# -s 参数生成 source map 文件
$ npx babel src -d lib -s

所以source map文件有什么作用呢?主要是为了方便定位打包后代码存在的问题。 注意: 1、直接npm install babel-cli的问题:包被错误的添加到"dependencies": {}里面 2、安装在全局:npm install --global babel-cli 其他人想要运行也必须全局安装,不支持不同项目运行不同的版本。 为了避免以上两种问题采用npm install --save-dev安装

初始化package.json文件

npm init

一路回车后,package.json文件创建好了 当然要记得删除之前安装的

$ npm uninstall  babel-cli
$ npm install --save-dev babel-cli

执行下面代码

npx babel example.js -o compiled.js

转码结果

'use strict';
var res = [123456].find(function (item) {
  return item > 4;
});
console.log(res);
var arr = [123456].map(function (item) {
  return item * 2;
});
console.log(arr);

最后改写package.json的script部分

{
  "name""babeltest",
  "version""1.0.0",
  "description""",
  "main""example.js",
  "dependencies": {},
  "devDependencies": {
    "babel-cli""^6.26.0",
    "babel-preset-latest""^6.24.1"
  },
  "scripts": {
    "build""babel src -d lib -s"
  },
  "author""",
  "license""ISC"
}

执行转码命令

npm run build

再来试试react转码规则,先修改.babelrc

{
  "presets": [
    "latest",
    "react"
  ],
  "plugins": []
}

再安装react转码规则

npm install --save-dev babel-preset-react

可以看到package.json的devDependencies变成了下面这样

"devDependencies": {
    "babel-cli""^6.26.0",
    "babel-preset-latest""^6.24.1",
    "babel-preset-react""^6.24.1"
  },

增加test.jsx文件写入以下代码

function component(){
  let isShow = true;
  return <div>
    {isShow && <div>我显示了</div>}
  </div>
}
component()

执行以下代码转码试试

npx babel test.jsx

转码结果

"use strict";

function component() {
  var isShow = true;
  return React.createElement(
    "div",
    null,
    isShow && React.createElement(
      "div",
      null,
      "\u6211\u663E\u793A\u4E86"
    )
  );
}
component();

1.6.3 babel-node

npx babel-node
(x=>x+1)(0)
1

修改package.json的script部分

"scripts": {
    "script-name""babel-node example.js"
  },

执行以下命令,可输出结果

npm run script-name

1.6.4 babel-register

babel-register 模块require加载时都会先进行Babel转码 先安装

npm install --save-dev babel-register

这样编写,就不需要对example.js进行转码

require ("babel-register");
require('./example.js')

1.6.5 babel-core

调用 Babel API 进行转码

$ npm install babel- core --save

然后这样子调用

var babel = require ( ’ babel-core ’ ) ;

// 字符亭转码
babel.transform ( 'code();', options); 
// => { code, map , ast } 
//文件转码(异步)
babel . transformFile ( 'filename.s', options, function(err, result) { 
  result; //  => { code, map, ast } 
} ) ; 
// 文件转码(同步)
babel . transformFileSync ( 'filename. js ', options); 
// => { code , map , ast }
// Babel AST 抽象语法树转码
babel.transformFromAst(ast , code , options) ; 
//  => { code , map , ast }

例如 在example.js输入以下代码

var babel = require ( 'babel-core' ) ;
var es6Code = 'let x = n => n + 1';
var esSCode = babel.transform(es6Code , { 
presets: ['latest'] 
}) 
. code;
console.log(esSCode)

使用node example.js执行后输出

"use strict";

var x = function x(n) {
  return n + 1;
};

1.6.6 babel-polyfill

Babel 默认只转换新的 JavaScript 句法( yntax ),而不转换新的 API ,如 Iterator Generator Set Maps Proxy Reflect Symbol Promise 等全局对象,以及一些定义在全局对象上的方法(如 Object assign )都不会转码。 所以需要使用babel-polyfill为当前环境提供一个垫片。 安装命令

$ npm install --save babel-polyfill

使用

import 'babel- polyfill';
// 或者
require ( 'babel-polyfill');

第2章 let 和 const 命令

2.1 let命令

2.1.1 基本用法

ES6新增let 命令,用于声明。只在命令所在代码块内生效。

{
    let a = 1;
    var b = 2;
    c = 3;
}
console.log(c); // 3
console.log(b); // 2
console.log(a); // Uncaught ReferenceError: a is not defined

所以简而言之,ES6就是来收拾之前ES5留下的一堆烂摊子的。

for(let i= 0;i<10;i++){};
console.log(i) // 报错Uncaught ReferenceError: i is not defined

只在命令所在代码块内生效,所以在循环体外使用报错。

var a= [];
for(var i= 0;i<10;i++){
    a[i]=function(){
        console.log(i)
    }
}
a[6](); // 10(想要的结果是6才对)

因为当调用a[6]这个函数时,i已经变为了10;我们可以用一个常量b先把i记录下来

var a= [];
for(var i= 0;i<10;i++){
    let b= i; // 当i为6时,b也被记录为6了,而且各个循环中的b不会互相干扰
    a[i]=function(){
        console.log(b)
    }
}
a[6](); // 6

或者我们直接更近一步,根本不需要变量b来存储记录

var a= [];
for(let i= 0;i<10;i++){
    a[i]=function(){
        console.log(i)
    }
}
a[6](); // 6

2.1.2 不存在变量提升

收拾另一个烂摊子:即变量可以在声明之前使用,值为 undefined。

console.log(a) // undefined
var a= 1;

相当于

var a
console.log(a) // undefined
a= 1;

这里就应该报错才对呀!为什么输出undefined

console.log(b)// 报错Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 1;

谢天谢地,let修复了这个问题。

2.1.3 暂时性死区

var tmp = 1;
{
  II TDZ 开始
 tmp = '666'; // 报错 Uncaught ReferenceError: Cannot access 'tmp' before initialization
 let tmp; // 当使用let重新声明时,不能在该代码块声明前使用tmp,TDZ 结束
}

在代码块内, 使用 let 命令声明变量之前,该变量都是不可用的。这在语法上称为“暂时性死区”( temporal dead one ,简称 TDZ )。 隐蔽的死区:

function returnArr(x = y,y = 2){
    return [x,y]
}
returnArr() // 报错 Uncaught ReferenceError: Cannot access 'y' before initialization

相当于

var x,y;
function returnArr(){
    let x = y;
    let y = 2;
    return [x,y]
}
returnArr() // 报错 Uncaught ReferenceError: Cannot access 'y' before initialization

应当改为

function returnArr(x = 2,y = x){
    return [x,y]
}
returnArr() // [2,2]

相当于

var x,y;
function returnArr(){
    let x = 2;
    let y = x;
    return [x,y]
}
returnArr()

再例如

let x= x; // 报错Uncaught ReferenceError: Cannot access 'x' before initialization

2.1.4 不允许重复声明

例子:

{
    let x = 1;
    var x = 2; // 报错 Uncaught SyntaxError: Identifier 'x' has already been declared
}
{
    let x = 1;
    let x = 2;// 报错 Uncaught SyntaxError: Identifier 'x' has already been declared
}
function func(arg){
    let arg; //报错 Uncaught SyntaxError: Identifier 'arg' has already been declared
}

改正上一个例子

function func(arg){
    {
        let arg; // 增加一个代码块就不报错了
    }
}

事实上可以重复声明,但不能在同级代码块声明。

2.2 块级作用域

2.2.1 为什么需要块级作用域

没有块级作用域可能导致的问题: 1、内层变量覆盖外层变量。

var tmp = 1;
function f(){
    console.log(tmp);
    if(false){
        var tmp = '666'
    }
}
f(); // undefined

相当于

var tmp = 1;
function f(){
    var tmp; // 相当于在函数内自己重新声明
    console.log(tmp);
    if(false){ // 这个花括号无法阻止变量提升到作用域顶端
        tmp = '666'
    }
}
f();

2、用来计数的循环变量泄露为全局变量。

for(var i= 0;i<10;i++){};
console.log(i) // 10(泄露了!)

2.2.2 ES6的块级作用域

let const 为js新增了块级作用域。

var tmp = 1;
function f(){
    console.log(tmp);
    if(false){ // 在let的作用下变成了块级作用域,阻止了变量提升
        let tmp = '666'
    }
}
f();  // 1

ES5允许块级作用域任意嵌套。

{{{let x = 1}}}

以前的js块级作用域IIFE写法

(function (){
    var tmp = 1;
}())

也就是立即执行的匿名函数。 ES6的块级作用域

{
    let tmp = 1;
}

2.2.3 块级作用域与函数声明

ES规定,函数只能在顶层作用域声明,不能在块级作用域中声明。 1、禁止情形一

if(true){
    function f(){}    
}

2、禁止情形二

try{
    function f(){}
}catch(e){}

遗憾的是浏览器没有遵循上面的约定。 浏览器ES6环境下

function f(){console.log('这是外面')}
(function (){
    if(false){
        function f(){console.log('这里是里面的')}
    }
    f()
}()) // 报错 Uncaught TypeError: f is not a function

因为相当于执行以下代码

function f(){console.log('这是外面')}
(function (){
    var f = undefined; 
    if(false){
        function f(){console.log('这里是里面的')}
    }
    f()
}())

浏览器允许块级作用域中声明函数(虽然应该不允许才对),类似var声明,会变量提升至作用域头部。 所以应当避免在块级作用域中声明函数,如果确实需要,应当使用函数表达式。

{
    let f = function(){}
}
function f(){console.log('这是外面')}
(function (){
    if(false){
        let f = function (){console.log('这里是里面的')}
    }
    f()
}()) // 输出 '这是外面'

而且必须使用大括号

"use strict"
if(true) function f(){}// 报错 Uncaught SyntaxError: In strict mode code, functions can only be declared at top level or inside a block.
"use strict"
if(true) {function f(){}} // 不报错

2.3 const 命令

2.3.1 基本用法

const 声明的变量只读,不能改变否则报错。

const PI = 3.14;
console.log(PI); // 3.14
PI = 3; // 报错 Uncaught TypeError: Assignment to constant variable.

声明后必须立即初始化,不能留到以后

const a; // 报错 Uncaught SyntaxError: Missing initializer in const declaration

只在声明所在作用域有效,没有变量提升,存在暂时性死区,只能声明后使用,不可重复声明。例子不再一一列举。

2.3.2 本质

const 实际上保证的是变量指向的内存地址不变。对于简单值意味着不能改变该变量,对于复合类型(主要是对象和数组)只能保证指针不变。

const obj={};
obj.a = 1;
console.log(obj.a) // 1
obj = {}; // 报错 Uncaught TypeError: Assignment to constant variable.(改变内存地址报错)

可以修改属性,但是改变指针时则会报错。

const arr =[];
arr.push(0); // 可执行
arr.length = 0;// 可执行
arr = ['1']; // 报错

如果真的要完全冻结呢?使用Object.freeze方法

const obj = {a: 1};
Object.freeze(obj)
obj.a = 2;
obj.c = 3;
console.log(obj) // {a: 1}(没有任何改变)
const obj = {a: {b:1}};
Object.freeze(obj)
obj.a.b = 2
console.log(obj) // {a: {b: 2}}(对象的属性的属性还是可以改变)

所以需要一个将对象切蒂冻结的函数。

const obj = {a: {b: 1}}
var constantize =(obj)=>{
    Object.freeze(obj);
    Object.keys(obj).forEach((key,i)=>{
        if(typeof obj[key] === 'object'){
            constantize(obj[key])
        }
    })
}
constantize(obj);
obj.a.b = 2;
console.log(obj) // {a: {b:1}}(被冻结没有改变了)

2.3.3 ES6声明变量的6中方法

ES6的6中声明变量方法: var命令、function命令、let命令和const命令、import命令和class命令。

2.4 顶层对象的属性

var a = 2;
console.log(window.a) // 2

顶层对象的属性与全局变量相关,被认为时js最大的设计败笔之一。 ES6又来收拾烂摊子了。

let b = 1;
console.log(window.b) // undefined

第3章 变量的解构赋值

3.1.1 基本用法

ES6从数组和对象中提取值,并赋值,称为解构。

let [a, b, c] = [1, 2 , 3];
console.log(a,b,c) // 输出 1,2,3 
let [d,[[[e]]]] = [1,[[[2]]]];
console.log(d,e) // 1,2

只要模式一样都能把值掏得出来。如果有些值不想取出,就干脆空着。

let [ , , f] = [1,2,3];
console.log(f) // 3

如果变量比值多了呢?

let [g ,h] = [1,];
console.log(g,h) // 1,undefined(多的那个变成了undefined

如果想取出一个或者多个,其他依然作为数组

let [i,...j] = [1,2,3];
console.log(i,j) // 1,[2,3]

如果其他作为数组的部分为空呢?

let [k,...l] = [1];
console.log(k,l) // 1,[](变成了空数组)

以下写法报错,...n形式必须放在最后

let [m,...n,o] = [1,2,3];// 报错Uncaught SyntaxError: Rest element must be last element

解构模式不一样报错,其实具体是因为等号另一边不具备Iterator接口

let [m] = 1; // 报错 Uncaught TypeError: 1 is not iterable

看看这个例子

let [n,o] = 'lzw';
console.log(n,o) // l,z

看起来不一样的模式但是依然解构出来了,因为字符串也有Iterator接口。Iterator就是为了提供一种统一的接口机制。任何的数据结构,只要部署了Iterator接口,便可以使用类似的方式完成遍历操作。 再例如下面这个例子

let [a] = new Set(['a','b']);
console.log(a) // 'a'

再看看更具体的例子。

function* autoIncrement(){ // Generator函数原生具有Iterator接口
    let a = 0;
    while(true){
        yield a++;
    }

}
let [a,b,c,d,e,f,g] = autoIncrement()
console.log(a,b,c,d,e,f,g) // 1,2,3,4,5,6

本来这个函数没有*,即不是Generator函数,会死循环,但是现在Generator函数原生具有Iterator接口,可以逐步去操作输出yield(即产出)结果。

3.1.2默认值

解构可以指定默认值。

let [a = 1] = [];
console.log(a) // 1
let [b= 1,c = 2] = [3,undefined];
console.log(b,c) // 3,2
let [b= 1,c = 2] = [3,null];
console.log(b,c) // 3,null

undefined会导致解构后变为默认值,null则不会。undefined的意思为应该有值缺未赋值,null意思为值为空null。 默认值甚至可以是函数,需要时再执行。

let a = 0;
function func(){
    return a++
}
let [b= func()] = [];
let [c= func()]=[];
console.log(b,c) // 0,1

默认值可以引用其他解构的默认值

let [a=1,b=a] = [];
console.log(a,b) // 1
let [a=b,b=1] = []; // 报错 Uncaught ReferenceError: Cannot access 'b' before initialization
console.log(a,b)

3.2 对象的解构赋值

let {a,b} = {a: 1,b:2};
console.log(a,b) // 1,2

当然这是没有顺序要求的,只要属性名一样。 如果变量名和属性名不一样,必须使用别名。

let {a:c,b:d} = {a: 1,b:2};
console.log(c,d) // 1,2

解构嵌套对象

let {a:{b:[c]}} = {a:{b:[1]}};
console.log(c) // 1

注意上面的a和b是无法打印出来的因为它们是模式,并不是变量。如果想打印出来怎么办,参考下面例子,再解构一次变量b

let {a:{b,b:[c]}} = {a:{b:[1]}};
console.log(b,c) // [1] 1

嵌套赋值的例子

let obj ={},arr = [];
({a:obj.a, b:arr[0]} = {a:1,b:2});
console.log(obj,arr) // {a: 1} [2]

对象解构指定默认值

let {a = 0,b=2} ={a: 1}
console.log(a,b) // 1 2

使用别名时的默认值

let {a:c = 0,b:d=2} ={a: 1}
console.log(c,d) //1 2

同样的默认生效条件时对象属性值严格等于undefined。

let {a=1,b=2,c= 3} = {b: null,c: undefined}
console.log(a,b,c) // 1 null 3

解构失败变量值为undefined

let {a} = {};
console.log(a) // undefined

如果元素对应的对象不存在则报错

let {a: {b}} = {c: 1}; // 报错 Uncaught TypeError: Cannot destructure property `b` of 'undefined' or 'null'.

已经声明的变量用于解构,容易报错。

let x;
{x} = {x: 1}; // 报错 Uncaught SyntaxError: Unexpected token =

正确写法

let x;
({x} = {x: 1});

用对象模式解构数组

let arr= [1,2,3];
let {0:first,[arr.length-1]:last} = arr;
console.log(first,last) // 1 3

3.3 字符串的解构赋值

let [a,b,c,d] = 'leezhenwang';
console.log(a,b,c,d) // l e e z
let {length} = 'leezhenwang';
console.log(length) // 11

3.4 数值和布尔值的解构赋值

let {toString : s} = 123 ; 
s === Number.prototype.toString // true
let {toString: s} = true; 
s === Boolean.prototype.toString // true

把toString属性解构出来,右边会先被转化为对象。undfined和unll无法转为对象,所以对它们进行解构赋值会报错。

let {a} = undefined; // 报错 Uncaught TypeError: Cannot destructure property `a` of 'undefined' or 'null'.
let {b} = null; // 报错 Uncaught TypeError: Cannot destructure property `b` of 'undefined' or 'null'.

3.5 函数参数的解构赋值

函数参数可以使用解构赋值

function add([a,b]){
    return a + b
}
add([1,2]) // 3

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

function returnArr({x=0,y=0} = {}){
    return [x,y]
}
console.log(returnArr({x:1,y:2})); // [1, 2]
console.log(returnArr({x:1})); // [1, 0]
console.log(returnArr({y:2})); // [0, 2]
console.log(returnArr({})); // [0, 0]
console.log(returnArr()); // [0, 0]

解构为undefined时取默认值,即不传参数时才取默认值{},这里是第一层解构。解构x, y时,解构失败undefined取默认值0,这里是第二层解构。两层解构。

function returnArr({x,y} = {x: 0,y: 0}){
    return [x,y]
}
console.log(returnArr({x:1,y:2})); // [1, 2]
console.log(returnArr({x:1})); // [1, undefined]
console.log(returnArr({y:2})); // [undefined, 2]
console.log(returnArr({})); // [undefined, undefined]
console.log(returnArr()); // [0, 0]

同样记住解构为undefined时取默认值,即不传参数时才取默认值 {x: 0,y: 0},这里是第一层解构。解构x, y时,解构失败undefined取默认值0,这里是第二层解构,但是并没有默认值,只能取undefined。两层解构。 看下面这个触发默认值的例子

[1,undefined,3].map((x=2)=>{
    return x
}) // [1, 2, 3]

3.6 圆括号问题

3.7 用途

第4章 字符串的拓展

4.1 字符的Unicode表示法

console.log("\u{20BB7}") // 吉

4.2 codePointAt()

需要四个字节储存的字符,js会认为它们时两个字符。

var s = "吉";
console.log(s.length); // 2
console.log(s.charAt(0)); // ''
console.log(s.charAt(1)); // ''
console.log(s.charCodeAt(0)); // 55362
console.log(s.charCodeAt(1)); // 57271

ES6提供codePointAt方法处理四个字节的字符,返回一个字符的码点。

var s = "吉a";
console.log(s.codePointAt(0)) // 134071
console.log(s.codePointAt(1)) // 57271
console.log(s.codePointAt(2)) // 97

4.3 String.fromCodePoint()

ES6提供了String.fromCodePoint方法,可识别大于0xFFF字符

String.fromCodePoint(0x20BB7) // "吉"

4.4 字符串遍历器接口

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

for(let w of 'lzw'){
    console.log(w)
} // 'l' 'z' 'w'
var text1 = String.fromCodePoint(0x20BB7);
for(let i= 0; i< text1.length;i++){
    console.log(text1[i]) // ' ' ' '
}
for(let i  of text1){
    console.log(i) // '吉'
}

相较于for循坏,for of会正确识别出这个字符。

4.5 at()

4.6 normalize()

4.7 includes()、startWith()、endsWith()

includes(): 返回布尔值,表示是否含有某个字符。 startsWith(): 返回布尔值,表示头部是否为参数字符。 endsWith(): 返回布尔值,表示末尾是否为参数字符。

var s = 'lzw';
console.log(s.includes('z')); // true
console.log(s.startsWith('l')); // true
console.log(s.endsWith('w')); // true

这三个方法都支持第二个参数

var s = 'leezhenwang';
console.log(s.includes('n',5)); // true(从前往后,切除前部后剩'nwang'再判断)
console.log(s.startsWith('e',5)); // true(从前往后,切除前部后剩'nwang'再判断)
console.log(s.endsWith('zh',5)); // true(从后往前,切除后部后剩'leezh'再判断)

4.8 repeat()

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

'n'.repeat(4) // 'nnnn'

小数取整

'n'.repeat(2.5) // "nn"

负数和Infinity报错

'n'.repeat(-1) // 报错 Uncaught RangeError: Invalid count value
'n'.repeat(Infinity) // 报错 Uncaught RangeError: Invalid count value

0到-1的小数等同零,NaN等同0

'n'.repeat(-0.1) // ""
'n'.repeat(NaN) // ""

字符串会转换为数字

'n'.repeat('llll') // ""
'n'.repeat('2') // "nn"

4.9 padStart()、padEnd()

长度不够时前部或者后部用某些字符串补全

'lzw'.padStart(6,'6') // "666lzw"
'lzw'.padEnd(6,'6') // "lzw666"

长度足够时,返回原字符

'lzw'.padEnd(3,'6') // "lzw"
'lzw'.padStart(3,'6') // "lzw"

补全字符超过所需长度,会截断补全

'l'.padStart(4,'123456') // "123l"

如果忽略第二个参数

'lzw'.padStart(5); // "  lzw"

padStart的常见用途一:补全指定位数

'1'.padStart(10,'0') // "0000000001"

4.10 模板字符串

传统的拼接字符串的输出模板太过麻烦。所以引入了模板字符串来解决这个问题。

let count = 100;
document.getElementById('ntp-contents').innerHTML = `
    <div>
        <p>这里是我新加入的文字</p>
        <span>数量是${count}<span>
    </div>
`

输出

"
    <div>
        <p>这里是我新加入的文字</p>
        <span>数量是100<span>
    </div>

"

可换行,可使用${}插入变量,所有缩进都保留在输出中。 可以选择去除开头和末尾的换行

let count = 100;
document.getElementById('ntp-contents').innerHTML = `
    <div>
        <p>这里是我新加入的文字</p>
        <span>数量是${count}<span>
    </div>
`.trim();

输出

"<div>
        <p>这里是我新加入的文字</p>
        <span>数量是100<span>
    </div>"

${}里面可以写表达式或者调用函数,反正就有返回值就行。

let a = 10,b= 22;
function returnNum(){
    return 100;
}
let str1 = `数量1是:${a+b}`;
let str2 = `数量2是:${returnNum()} `
console.log(str1,str2) // '数量1是:32' '数量2是:100'

未声明的变量

let str3 = `这个地方是${place}`; // 报错 Uncaught ReferenceError: place is not defined

如果是字符串,原样返回。

let str4 = `这个地方是${'广州'}`; // '这个地方是广州'

模板字符串还能嵌套

let arr = [1,2,3,4,5];
document.getElementById('ntp-contents').innerHTML = `
    <ul>
        ${arr.map(item=>`<li>数量是${item}</li>`).join('')}
    </ul>
`.trim();

输出

"<ul>
        <li>数量是1</li><li>数量是2</li><li>数量是3</li><li>数量是4</li><li>数量是5</li>
    </ul>"

4.11 实例:模板编译

4.12 标签模板

4.13 String.raw()

第5章 正则的拓展

5.1 RegExp 构造函数

new RegExp(/abc/ig,'i').flags // "i"

可以使用第二参数作为修饰符,而且会覆盖原有修饰符。

5.2 字符串的正则方法

字符串对象共有4个方法可以使用正则表达式:match()、repeat()、search()和split()。

5.3 u修饰符

u修饰符用来正确处理大于\uFFFF的Unicode字符。

/^\uD83D/u.test('\uD83D\uDC2A') // false

5.4 y修饰符

var s = 'aaa_aa_a_';
var reg = /a+_/y;
reg.exec(s); // ["aaa_", index: 0, input: "aaa_aa_a_", groups: undefined]
reg.exec(s); // ["aa_", index: 0, input: "aaa_aa_a_", groups: undefined]
reg.exec(s); // ["a_", index: 0, input: "aaa_aa_a_", groups: undefined]

必须是剩余匹配字符的首个开始匹配

5.5 sticky属性

表示是否设置了y修饰符。

var r = /hello\d/y;
r.sticky; // true

5.6 flags属性

ES6增加了flags属性

/abc/ig.flags // "gi"

5.7 s修饰符: dotAll 模式

/foo.bar/s.test ( 'foo\nbar') // true (.匹配任意单个字符)

5.8 后行断言

先行断言: x只有在y前面才能匹配,必须写成/x(?=y)/的形式。 先行否定断言: x只有不在y面前才能匹配,必须写成/x(?!y)/的形式。

/\d+(?=%)/.exec('50%的人都买不起f') // ["50", index: 0, input: "50%的人都买不起f", groups: undefined]
/\d+(?!%)/.exec('50%的人都买不起400万的房') // ["5", index: 0, input: "50%的人都买不起400万的房", groups: undefined]

后行断言

/(?<=\$)\d+/.exec('我今天花了$100') // ["100", index: 6, input: "我今天花了$100", groups: undefined]

后行否定断言

/(?<!\$)\d+/.exec('我今天双11花了$100') // ["11", index: 4, input: "我今天双11花了$100", groups: undefined]

5.9 Unicode 属性类

5.10 具名组匹配

5.10.1 简介

正则表达式使用圆括号进行组匹配。

const date = /(\d{4})-(\d{2})-(\d{2})/ 
const matchObj = date.exec('2020-11-24');
const year = matchObj[1];
const month = matchObj[2];
const day = matchObj[3];
console.log(year,month, day) // 2020 11 24

具名匹配避免使用序号

const date = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = date.exec('1010-11-24');
const year = matchObj.groups.year;
const month = matchObj.groups.month;
const day = matchObj.groups.day;
console.log(year,month,day) // 1010 11 24

5.10.2 解构赋值和替换

解构具名匹配结果

let {groups:{one,two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar')
console.log(one,two) // foo bar

字符串替换时,使用$<组名>引用具名组

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re,'$<day>/$<month>/$<year>') // "02/01/2015"

第二个参数也可以是函数

'2015-01-02'. replace (re , ( 
    matched, //整个匹配结果 2015 01 02
    capturel , //第一个组 匹配 2015
    capture2 , //第 二个组匹配 01
    capture3 , //第三个组匹配 02
    position, //匹配开始的位
    s, // 原字符串 2015-01-02
    groups //具名纽构成的一个对象 year month , day}
) => {
    let {day , month , year} = groups ; 
    return `${day}/${month}/${year}`
}) ;

5.10.3 引用

正则表达式内部引用某个具名匹配,使用\k<组名>

const re = /^(?<word>[a-z]+)!\k<word>$/;
console.log(re.test('aaa!aa')) // fasle
console.log(re.test('aaa!aaa')) // true

依然可以使用\1,甚至\k、\1混用。

const re = /^(?<word>[a-z]+)!\1$/;
console.log(re.test('aaa!aa')) // fasle
console.log(re.test('aaa!aaa')) // true

第6章 数值的拓展

6.1 二进制和八进制的表示法

ES6使用前缀0B、0b表示二进制,0O、0o表示八进制。

0b11111 // 31
0o11111 // 4681

6.2 Number.isFinite()、Number.isNaN()

Number.isFinite()除了数值都返回false

console.log(Number.isFinite(15)) // true
console.log(Number.isFinite(NaN)) // false
console.log(Number.isFinite(Infinity)) // false
console.log(Number.isFinite('15')) // false
console.log(Number.isFinite(true)) // false
console.log(Number.isFinite(Math.PI)) // true(圆周率不应该是无穷数么)
console.log(Number.isFinite(1/3)) // true0.33333...不应该是无穷数么)

感觉没啥用 Number.isNaN()判断是不是和数值有关却不是数值的一类东西

console.log(Number.isNaN(15)) // false
console.log(Number.isNaN(NaN)) // true
console.log(Number.isNaN('15')) // false
console.log(Number.isNaN(true)) // false
console.log(Number.isNaN(Infinity)) // false
console.log(Number.isNaN(15/NaN)) // true
console.log(Number.isNaN(''/0)) // true
console.log(Number.isNaN(''/'')) // true (为啥字符串相除会是NaN)
console.log(Number.isNaN(true/'')) // false
console.log(Number.isNaN(''/true))// false

6.3 Number.parseInt()、Number.parseFloat()

ES6将全局方法移植到Number对象。

Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
Number.parseInt('12.5rrr') // 12
Number.parseFloat('12.5rrr') // 12.5

使语言逐步模块化。

6.4 Number.isInterger()

Number.isInterger()用来判断一个值是否为整数。注意,js整数和浮点数是同样的存储方法,所以小数点后为0会被认为是整数。

console.log(Number.isInteger(1)) // true
console.log(Number.isInteger(1.0)) // true
console.log(Number.isInteger('1')) // false
console.log(Number.isInteger(1.1)) // false
console.log(Number.isInteger(true)) // false

6.5 Number.EPSILON

ES6增加有个可以接受的误差范围,以解决0.1+ 0.2不等于0.3的问题。

0.1+0.2 === 0.3 // false
0.1+0.2-0.3 < Number.EPSILON // true

检查是否相等的

function checkEqual(num1,num2){
    return Math.abs(num1-num2) < Number.EPSILON
}
console.log(checkEqual(0.1+0.2,0.3)) // true
console.log(checkEqual(0.1+0.1,0.3)) // false

6.6 安全整数和Number.isSafeInterger()

js整数范围为-2的53次方到2的53次方(不含两个端点),超过这个范围无法精确表示。

Math.pow(2,53) // 9007199254740992
9007199254740992 + 1 // 9007199254740992(无法表示超过范围的数值所以还是最大数值)

ES6引入了Number.MAX_SAFE_INTEGER和Number_MIN_SAFE_INTEGER来表示这个上下限

Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MAX_SAFE_INTEGER === Math.pow(2,53)-1 // true(为啥要减一呢?估计是为了上下限的统一)
Number.MAX_SAFE_INTEGER === -Number.MIN_SAFE_INTEGER  // true

Number.isSafeInterger()判断一个整数是否落在这个范围内。

Number.isSafeInteger(Math.pow(2,53)) // false

6.7 Math对象拓展

6.8 Math.signbit()

6.9 指数运算

2**53 // 9007199254740992
let a= 2;
a**= 3;
console.log(a) // 8

6.10 Integer数据类型

第7章 函数的拓展

7.1 函数参数的默认值

7.1.1 基本用法

ES6可以设置默认参数

function saySomething(x,y= 'lzw'){
    console.log(x,y)
}
saySomething('Hi','mayun') // Hi mayun
saySomething('Hi') // Hi lzw
saySomething('Hi',undefined) // // Hi lzw

参数变量x默认声明

function func(x= 5,y){
    let y = 1; // y可以声明
    let x = 1; // 报错 Uncaught SyntaxError: Identifier 'x' has already been declared
}

参数默认值使用时再取值,惰性求值。

let b = 1;
function func1(x=b++){
    console.log(x)
}
func1(); // 1
func1(); // 2

7.1.2 与解构赋值默认值结合使用

function func({x=1,y=2} = {x: 5,y:6}){
    console.log(x,y)
}
func(); // 5 6(第一重,只有当没有参数时才采用{x: 5,y:6})
func({x: 3,y:4}) // 3 4
func({y:4}) // 1 4
func({x: 3}) // 3 2
func({x: undefined,y:undefined}) // 1 2
func({}) // 1 2

这里有双重默认值,第一重,只有当没有参数时才采用{x: 5,y:6},第二重,有参数时,undefined时取默认值。

7.1.3 参数默认值的位置

通常默认值应该再函数尾部,否则,这个参数是无法省略的。

function a(x = 1,y){
    console.log(x,y)
}
a(,1) // 报错 Uncaught SyntaxError: Unexpected token ,
function b(x,y = 1){
    console.log(x,y)
}
b(1) // 1 1(尾部默认值参数可以不传)

7.1.4 函数的length属性

length属性返回没有指定默认值的参数个数,一旦指定默认值,将失真。

(function a(x){}).length // 1
(function a(x = 1){}).length // 0(失真了)

7.1.5 作用域

一旦设置了参数的默认值,函数声明初始化时,参数会形成一个单独的作用域。初始化结束,作用域消失。

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

调用的时候再取值,因为存在一个单独作用域,所以y取的是2。

var x = 1;
function func(x = x){

}
func() // 报错 Uncaught ReferenceError: Cannot access 'x' before initialization

形成独立作用域相当于let x = x;当然会报错。

let foo = "outer";
function bar (func = x=>foo){
    let foo = "inner";
    console.log(func())
}
bar() // outer

相当于

let foo = "outer";
let func = x=>foo
function bar (){
    let foo = "inner";
    console.log(func())
}
bar() // outer

查找变量只会从函数定义作用域向上查找

// let foo = "outer";
function bar (func = x=>foo){
    let foo = "inner";
    console.log('进来了')
    console.log(func())
}
bar() // outer
// 进来了
// 报错 Uncaught ReferenceError: foo is not defined

7.2 rest参数

存放剩余的参数

function a(b,...c){
    console.log('第一个参数是:'+ b)
    console.log('其他参数是:'+c)
}
a(1,2,3,4,5,6)
// 第一个参数是:1
// 其他参数是:2,3,4,5,6

rest参数必须放在最后

function a(b,...c,d){ // 报错 Uncaught SyntaxError: Rest parameter must be last formal parameter
    console.log('第一个参数是:'+ b)
    console.log('其他参数是:'+c)
}
a(1,2,3,4,5,6)

函数length 属性不包括rest参数

(function a(b,...c){}).length // 1

7.3 严格模式

ES6规定函数参数只要使用了默认值,解构赋值或者扩展运算符,函数内部不能设为严格模式。

function doSomething(a = 1){
    'use strict';
} // 报错 Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list

解决方法一:全局性的严格模式

'use strict';
function doSomething(a = 1){

}

解决方法二: 使用无参数立即执行函数,包一层函数写入'use strict'

const doSomething = (function (){
    'use strict';
    return function(a = 1){
        console.log(a)
    }
}())
doSomething() // 1

7.4 name 属性

(function saySomething(){}).name // "saySomething"

bind返回的函数,name属性会加上bound前缀

function saySomething(){}
saySomething.bind({}).name // "bound saySomething"

7.5 箭头函数

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

 var f = v=> v;

等同于

var f = function (v){ return v }

不传参数时使用()

 var f = ()=> 1;

使用超过一个参数时

var add = (a,b) => a+b

执行的操作不仅仅是返回一个简单值时加{}

var add = (a,b) => {
    console.log(a+ b)
    return a+b
}
add(1,2) 
// 输出 3
// 返回 3

箭头函数结合解构

var add = ({a,b}) => a+b;
console.log(add({a:1,b:2})) // 3

箭头函数结合rest参数

var doSomething = (...a) => a;
console.log(doSomething(1,2,3,4)) // [1, 2, 3, 4]

7.5.2 注意事项

1、函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象。 2、不可当作构造函数,不可以使用new命令。 3、不可以使用arguments对象。 4、不可以使用yield命令,因此箭头函数不能作Generator函数。 第一点:

function foo (){
    setTimeout(()=>{console.log(this.id)},1000)
}
foo.call({id:1}); // 1

箭头函数导致this总是指向函数定义生效时所在的对象,所以输出1. 第二点

var foo = x=>{
    this.x = x
}
new foo(1) // 报错 Uncaught TypeError: foo is not a constructor

第三点

var foo = x=>{
    console.log(arguments)
}
foo() // 报错 Uncaught ReferenceError: arguments is not defined

7.5.3 嵌套的箭头函数

箭头函数内部还可以再使用箭头函数。

var foo = a=>b=>b+a;
foo(1)(2) // 3

7.6 绑定this

7.7 尾调用优化

7.7.1 尾调用

尾调用指某个函数的最后一步调用另一个函数。

function f(x){
    return g(x)
}

一下情况不属于尾调用 情况一

function f(x){
    let y = g(x)
    return y
}

情况二

function f(x){
    return g(x) + 1
}

情况三

function f(x){
    g(x)
}

必须调用函数是最后一步。

7.7.2 尾调用优化

函数调用会在内存形成一个“调用记录”,又称调用帧,保存调用位置和内部变量等信息。所有调用帧就形成了一个调用栈。 尾调用由于是函数最后一步的操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到,直接用内层函数的调用帧取代外层函数即可。

function f(){
    let m = 1;
    let n = 2;
    return g(m+n)
}
f()

等同于

function f(){
    return g(3)
}
f()

尾调用优化,只保留内层函数的调用帧。所以函数g内部不能使用外层函数内部变量。

7.7.3 尾递归

函数调用自身称为递归。如果尾调自身就称为尾递归。尾递归只存在一个调用帧,所以永远不会发生“栈溢出”错误。 阶乘函数

function factorial(n){
    if(n === 1) return 1;
    return n*factorial(n-1)
}
factorial(5) // 120

因为不是尾调用,所以复杂度为O(n)。理解:n*(n-1)*(n-2)*(n-3)*(n-4),每一个调用栈的内存先保留,容易溢出

function factorial(n,total){
    if(n === 1) return total;
    return factorial(n-1,n*total)
}
factorial(5,1) // 120

尾调用复杂度为O(1)。理解: toal = n*1;toal = total*(n-1);toal = total*(n-2);toal = total*(n-3);toal = total*(n-4),不需要管之前的步骤,只需要保留total值,而每次调用total已经通过参数传进来了。 如何改写为尾递归?通过增加中间变量,即增加参数来实现。

7.7.4 递归函数的改写

把所有内部用到的变量改写成函数参数。 柯里化,将多参数的函数换成单参数的形式。

function currying(fn,n){
    return function(m){
        return fn.call(this,m,n)
    }
}
function tailFactorial(n,total){
    if(n===1) return total;
    return tailFactorial(n-1,n*total)
}
const factorial = currying(tailFactorial,1);
factorial(5)

采用ES6函数默认值

function factorial(n,total =1){
    if(n === 1) return total;
    return factorial(n-1,n*total)
}
factorial(5) // 120

7.7.5 严格模式

ES6的尾调优化只有在严格模式下开启,正常模式下是无效的。

7.7.6 尾递归优化的实现

不开启严格模式下自己实现尾递归优化。尾递归之所以需要优化,原因是调用栈太多造成溢出,只要减少调用栈就不会溢出。可以采用循环代替递归。

function sum(y){
    if(y > 0){
        return sum(y-1)
    }else{
        return 1
    }
}
sum(100000) // 报错 Uncaught RangeError: Maximum call stack size exceeded

蹦床函数可以将递归函数转为循环执行。

function trampoline(f){
    while(f && f instanceof Function){ // 执行的结果是函数的话继续执行,一直到不再是函数为止
        f= f()
    }
    return f;
}

这里是返回一个函数然后执行函数,而不是再函数里调用函数,避免了递归执行,消除调用栈过大的问题。

function trampoline(f){
    while(f && f instanceof Function){
        f= f()
    }
    return f;
}
function sum(x,y){
    if(y > 0){
        return sum.bind(null,x+1,y-1) // 使用biand函数返回一个未执行的函数而且可以传递参数等待执行
    }else{
        return x
    }
}
trampoline(sum(1,100000)) // 100001

真正的尾递归优化

function tco(f){
    var value;
    var active = false;
    var accumulated = []; // 闭包存储的数据,x结果实际上存在了这里
    return function accumulator(){ // 这个函数赋给了sum
        accumulated.push(arguments);
        if(!active){
            active = true;
            while(accumulated.length){ // 每次执行一次,因为后面立马把参数去除
                value = f.apply(this,accumulated.shift()) // 这里把参数去除并返回sum
            }
            active = false;
            return value; // 真正结果出来前返回的是undefined
        }
    }
}
var sum = tco(function(x,y){
    if(y>0){
        return sum(x+1,y-1)
    }else{
        return x
    }
})
sum(1,100000) // 100001

7.8 函数参数的尾逗号

sum(1,100000,) // 100001

第8章 数组的拓展

8.1 拓展运算符

拓展运算符是三个点(...),将一个数组转为用逗号分隔的参数序列。

console.log(...[1,2,3]) // 1 2 3

将数组拆分为参数

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

随意组合新数组

let a=[1,2],b=[3,4],c=5,d;d=[...a,...b,c];console.log(d); //  [1, 2, 3, 4, 5]

拓展运算符后面放置表达式

let a =1,b=[2,3],c=[4,5]
console.log(...(a> 0 ? b: c)) // 2 3

拓展运算符后面是空数组不产生任何效果。

[1,...[]] // [1]

8.1.2 替代数组的apply方法

function f(x,y,z){
    console.log(x,y,z)
}
var args = [1,2,3];
f.apply(null, args); // 1 2 3

替代后

function f(x,y,z){
    console.log(1,2,3)
}
var args = [1,2,3];
f(...args) // 1 2 3

8.1.3 拓展运算符的应用

合并数组

let arr = [1,2];
console.log([0,...arr]) // [0, 1, 2]

与解构赋值结合

let [a,b,...c] = [1,2,3,4,5]
console.log(a,b,c) // 1 2 [3, 4, 5]

拓展运算符必须放在最后一位,否则报错

let [a,...b,c] = [1,2,3,4,5] // 报错 Uncaught SyntaxError: Rest element must be last element
console.log(a,b,c) 

字符串转为数组

console.log([...'leezhenwang']) // ["l", "e", "e", "z", "h", "e", "n", "w", "a", "n", "g"]

实现Iterator接口对象

var nodeList = document.querySelectorAll('div') // 具有Iterator接口
console.log([...nodeList]);

没有的只能通过Array.from()方法转换为真正的数组。 Map和Set结构、Generator函数 拓展运算符内部调用的是数据结构的Iterator Map结构

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

Generator函数运行后会返回一个遍历器对象,因此可以使用拓展运算符。

function*go(){
    yield 1;
    yield 2;
    yield 3;
}
[...go()] //  [1, 2, 3]

8.2 Array.from()

Array.from方法用于将两类对象转换为真正的数组:类似数组的对象和可遍历的对象,包括数据结构Set和Map。 类似数组的对象 ES5写法

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
}
var arr1 = [].slice.call(arrayLike);  // [].slice()浅复制数组
console.log(arr1) // ["a", "b", "c"]

ES6写法

let arr2 = Array.from(arrayLike) // ["a", "b", "c"]

可遍历对象例如有Iterator接口的数据结构

Array.from('lzw') // ["l", "z", "w"](字符串具有Iterator接口)
let nameset = new Set(['a','b'])
Array.from(nameset) // ["a", "b"]

数组会原样返回

Array.from([1,2,3,4]) // [1, 2, 3, 4]

拓展运算符(...)将某些数据结构转为数组。

function foo(){
    var args = [...arguments];
    console.log(args)
}
foo(1,2,3,4,5,6,7,8) // [1, 2, 3, 4, 5, 6, 7, 8]
[...document.querySelectorAll('div')]

转换类似数组对象

Array.from({length: 3}) // [undefined, undefined, undefined]

Array.from的第二个参数,相当于map方法

Array.from([1,2,3],x=>x**2) // [1, 4, 9]

处理类数组

let divs = document.querySelectorAll('div');
let ids = Array.from(divs, s=>s.id)
console.log(ids) // ["custom-bg", "one-google", "ntp-contents", "logo", "logo-default"]

8.3 Array.of()

Array.of(1,2,3) // [1, 2, 3]

因为Array()函数表现得很怪异,参数不同返回值不一样

Array(1,2,3) // [1, 2, 3]
Array(3) // [empty × 3]

8.4 数组实例的copyWithin()

复制指定位置成员到其他位置,会修改原数组。 Array.prototype.copyWithin(target,start = 0, end=this.length) target:必选开始替换数据的位置 start:可选开始读取数据位置,负数时,为-start+length end: 可选,停止读取数据的位置,默认为0;负数时,为-end+length

[1,2,3,4,5].copyWithin(0,4) // [5, 2, 3, 4, 5]
[1,2,3,4,5].copyWithin(0,-3,-2) // [3, 2, 3, 4, 5](位置为-n+length)
[1,2,3,4,5].copyWithin(0,-2,-3)  // [1, 2, 3, 4, 5](没有按坐标轴规则的返回原数组)

8.5 数组实例的find() 和findIndex()

[1,2,3,4,5].find(item=>item >= 4) // 4

回调参数可以传入三个参数

[1,2,3,4,5].find(function(item,index,arr){
    if(item >= 4){
        console.log(item,index,arr) // 4 3 [1, 2, 3, 4, 5]
        return true
    }
    return false
}) // 返回值 4

返回的是其中一项 findIndex返回索引值

[1,2,3,4,5].findIndex(function(item,index,arr){
    if(item >= 4){
        console.log(item,index,arr) // 4 3 [1, 2, 3, 4, 5]
        return true
    }
    return false
}) // 3

主要是为了解决indexOf无法识别NaN的不足。

8.6 数组实例的fill()

['a','b','c'].fill(7,1,2) // ["a", 7, "c"](填充位置类似数学上的[1,2))

8.7 数组实例的entries()、keys()和values()

entries()、keys()和values()可用于遍历数组

for (let index of ['a','b'].keys()){
    console.log(index);
} // 0 1
for (let item of ['a','b'].values()){
    console.log(item);
} // 'a' 'b'
for (let obj of ['a','b'].entries()){
    console.log(obj);
}
// [0, "a"]
// [1, "b"]
for (let [index,item] of ['a','b'].entries()){
    console.log(index,item);
} 
// 0 "a"
// 1 "b"

手动调用遍历对象的next方法进行遍历

let letter = ['l','z','w'];
let entries = letter.entries();
console.log(entries.next().value) // [0, "l"]
console.log(entries.next().value) // [1, "z"]
console.log(entries.next().value) // [2, "w"]

8.8 数组实例的includes()

表示是否包含给定的值。

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

第二个参数表示搜索的起始位置,默认值为0。为负数时,-n+length,如果-n+length<0,重置为默认值。

[1,2,3].includes(2,1) // true
[1,2,3].includes(2,-2) // true(相当于上面的写法)
[1,2,3].includes(2,-1000) // true

解决indexOf不够语义化,需要比较是否等于-1,内部使用严格===判断的问题 indexOf的误判

[NaN].indexOf(NaN) // -1

8.9 数组的空位

第9章 对象的拓展

9.1 属性的简介表示法

ES6允许直接写入变量和函数作为对象的属性和方法。

var foo = 'bar';
var obj = {foo} // 相当于{foo:foo}
console.log(obj) // {foo: "bar"}
var foo = ()=>{};
var obj = {foo}
console.log(obj) // {foo: ƒ}

方法简写

var o={
    method(){
        return 1
    }
}

相当于

var o={
    method:function(){
        return 1
    }
}

方法是Generator函数时

var o={
    *method(){
        yield 1
    }
}

9.2 属性名表达式

使用字面量方式定义对象

var o={
    ['a'+'bc']: 123
}
console.log(o) // {abc: 123}
var o={
    ['a'+'bc'](){}
}
console.log(o) // {abc: ƒ}

属性名表达式和简介表示法不能同时使用

var foo = 'bar';
var bar = 'abc';
var baz = {[foo]} // 报错

属性名最好不要是对象,否则会转为字符串,覆盖之前的对象属性

var a= {};
var b = {};
var o={
    [a]: 'a',
    [b]: 'b'
}
console.log(o) // {[object Object]: "b"} (前面的被覆盖了)

9.3 方法的name属性

对象方法也有name属性。

const person = {
    sayName(){
        console.log('hello')
    }
}
person.sayName.name // "sayName"

9.4 Object.is()

比较两个值是否严格相等

Object.is('foo','foo') // true
Object.is({},{}) // false(内存地址不一致)
Object.is(+0,-0) // false
Object.is(NaN,NaN) // true

9.5 Object.assign()

9.5.1 基本用法

第一个参数是目标对象,后面的参数都是源对象

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

后面的参数属性会覆盖前面的参数属性

var target = { a: 1};
var source1 = { b: 2};
var source2 = { b: 4,c: 3};
Object.assign(target,source1,source2) // {a: 1, b: 4, c: 3}(b被后面的覆盖了)

只有一个参数且是对象直接返回

var target = { a: 1};
Object.assign(target) // {a: 1}

非对象会转换为对象

Object.assign(2) // Number {2}

undefined和null无法转换成对象

Object.assign(null) // Uncaught TypeError: Cannot convert undefined or null to object
Object.assign(undefined) // Uncaught TypeError: Cannot convert undefined or null to object

但是null和undefined出现在非首个参数位置的其他位置则不会报错。

Object.assign({a:1},null) // {a: 1}
Object.assign({a:1},undefined) // {a: 1}

Object.assign不复制继承属性,也不复制不可枚举的属性。

let obj = Object.defineProperty({},'invisible',{
    enumerable: false,
    value: 'hello'
}) // {invisible: "hello"}
Object.assign({b:'c'},obj) // {b: "c"}

Symbol值属性也会被Object.assign复制

Object.assign({a:'b'},{[Symbol('c')]: 'd'}) // {a: "b", Symbol(c): "d"}

9.5.2 注意点

Object.assign实行的是浅复制,如果源对象的某个属性的值是对象那么目标对象得到的是这个对象的引用。

var obj1 = {a: {b:1}};
var obj2 = Object.assign({},obj1)
obj1.a.b = 2;
obj2.a.b; // 2(第二层跟随源对象改变)
var obj1 = {a: {b:1}};
var obj2 = Object.assign({},obj1)
obj1.a = {};
obj2.a; // {b: 1}(第一层并不会跟随)

可以理解为第一层深拷贝,第二层浅拷贝。 Objetct.assign处理数组,但是会把数组视为对象来处理。

Object.assign([1,2,3,4,5],[7,8,9]) // [7, 8, 9, 4, 5] (覆盖相同位置的)

9.6 属性的可枚举

let obj = {a: 'b'}
Object.getOwnPropertyDescriptor(obj, 'a') // {value: "b", writable: true, enumerable: true, configurable: true}

4个操作会忽略enumerable为false的属性。 for...in循环:只遍历对象自身和继承的可枚举属性。(只有这个可以返回继承属性,操作继承属性会出现不必要的麻烦) Object.keys(): 返回对象自身的所有可枚举的键名。 JSOM.stringfy(): 只串行化对象自身的可枚举属性。 Object.assign(): 只复制对象自身的可枚举对象。 ES6所有Class原型方法都是不可枚举的。

Object.getOwnPropertyDescriptor(class {foo(){}}.prototype,'foo').enumerable // false

9.7 属性的遍历

ES6一共有5种方法可以遍历对象的属性。 1、for...in 遍历对象自身和继承的可枚举属性(不包含Symbol属性)

function foo (a){
    this.a = a;
}
var obj1 = {b: 2,c: 3,[Symbol('d')]: 4}
//Object.defineProperty(obj1,'c',{enumerable: false})
foo.prototype = obj1;
var obj = new foo(1);
for(key in obj){
    console.log(key)
} // a b c(包括继承的属性但是,不包括Symbol属性)
function foo (a){
    this.a = a;
}
var obj1 = {b: 2,c: 3,[Symbol('d')]: 4}
Object.defineProperty(obj1,'c',{enumerable: false}) // c不可枚举
foo.prototype = obj1;
var obj = new foo(1);
for(key in obj){
    console.log(key)
} //  a b (不包括不可枚举属性c)

2、Object.keys(obj) 返回一个数组,包括自身的所有可枚举属性,不包括继承的属性

function foo (a,b){
    this.a = a;
    this.b = b;
}
var obj1 = {c: 3}
foo.prototype = obj1;
var obj = new foo(1,2);
Object.defineProperty(obj,'b',{enumerable: false})
obj[Symbol('d')] = 4
Object.keys(obj) // ["a"](没有b,c,d因为b不可枚举,c继承属性该方法无法获取,d为Symbol属性无法获取)

3、Object.getOwmPropertyNames(obj) 返回一个数组,包含不可枚举的属性和可枚举的属性,不包含Symbol属性。

function foo (a,b){
    this.a = a;
    this.b = b;
}
var obj1 = {c: 3}
foo.prototype = obj1;
var obj = new foo(1,2);
Object.defineProperty(obj,'b',{enumerable: false})
obj[Symbol('d')] = 4
Object.getOwnPropertyNames(obj) // a b(包含不可枚举的b,不包含继承的属性和Symbol属性)

4、Object.getOwnPropertySymbols(obj) 返回一个数组,包含自身的所有Symbol属性。

function foo (a,b){
    this.a = a;
    this.b = b;
}
var obj1 = {c: 3,[Symbol('e')]: 5}
foo.prototype = obj1;
var obj = new foo(1,2);
Object.defineProperty(obj,'b',{enumerable: false})
obj[Symbol('d')] = 4
Object.getOwnPropertySymbols(obj) // [Symbol(d)]

5、Reflect.ownKeys(obj)

function foo (a,b){
    this.a = a;
    this.b = b;
}
var obj1 = {c: 3,[Symbol('e')]: 5}
foo.prototype = obj1;
var obj = new foo(1,2);
Object.defineProperty(obj,'b',{enumerable: false})
obj[Symbol('d')] = 4
Reflect.ownKeys(obj) //  ["a", "b", Symbol(d)]

9.8 _proto_属性、Object.setPrototypeOf()、Object.getPrototypeOf()

9.8.1 _proto_属性

_proto_属性用来读取和设置当前对象的prototype对象。 ES5写法

var obj = Object.create({a:1})
obj.b = 1;
obj; // {b: 1}(a属性在原型链上)

ES6写法

var obj= {b: 1};
obj.__proto__ = {a: 1}
obj;

尽量使用Object.setPrototypeOf()代替

9.8.2 Object.setPrototypeOf()

设置prototype对象,返回参数对象本身 格式:

Object.setPrototypeOf(obj,prototype) 
let proto = {};
let obj = {a: 1}
Object.setPrototypeOf(obj,proto)
proto.b = 2;
obj.b; // 2

9.8.3 Object.getPrototypeOf()

Object.getPrototypeOf(obj)获取对象的prototype对象。

let proto = {};
let obj = {a: 1}
Object.setPrototypeOf(obj,proto)
Object.getPrototypeOf(obj); // {}

9.9 Object.keys()、Object.values()、Object.entries()

不包括继承属性和Symbol属性

9.9.1 Object.keys()

for(let key of Object.keys({a:1,b:2})){
    console.log(key)
} // a b

9.9.2 Object.values()

for(let key of Object.values({a:1,b:2})){
    console.log(key)
} // 1 2

9.9.3Object.entries()

for(let key of Object.entries({a:1,b:2})){
    console.log(key)
} // ["a", 1] ["b", 2]

将对象转换为真正的Map结构

var map = new Map(Object.entries({a:1,b:2}))
map; // Map(2) {"a" => 1, "b" => 2}

9.10 对象的拓展运算符

解构赋值,...必须放到最后,是浅复制,对值的引用

let {a,b,...c} = {a:1,b:2,c:3,d:4}
console.log(a,b,c) // 1 2 {c: 3, d: 4}

9.11 Object.getOwnPropertyDescriptors()

const obj = {
    foo:123,
    get bar(){ return 'abc'}
}
Object.getOwnPropertyDescriptors(obj) // {foo: {…}, bar: {…}}

解决get和set属性正确复制的问题。

const source = {
    set foo(value){
        console.log(value)
    }
}
const target1 = {};
Object.assign(target1,source);
Object.getOwnPropertyDescriptor(target1,'foo') // {value: undefined, writable: true, enumerable: true, configurable: true}

复制失败,值为undefined,因为get方法未传入参数,不会复制set属性。

const source = {
    set foo(value){
        console.log(value)
    }
}
const target2 = {};
Object.defineProperties(target2,Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2,'foo') // {get: undefined, set: ƒ, enumerable: true, configurable: true}

复制成功,更简洁的写法是

const source = {
    set foo(value){
        console.log(value)
    }
}
const shallowMerge = (target,source)=> Object.defineProperties(target,Object.getOwnPropertyDescriptors(source))
shallowMerge({},source)

配合Object.create方法将对象属性克隆到一个新对象。这属于浅复制。

let obj1 = {
    set foo(value){
        console.log(value)
    }
}
let obj2 = {a: 1}
Object.setPrototypeOf(obj1,obj2)
const clone = Object.create(Object.getPrototypeOf(obj1),Object.getOwnPropertyDescriptors(obj1)) //可以复制set

或者

let obj1 = {
    set foo(value){
        console.log(value)
    }
}
let obj2 = {a: 1}
Object.setPrototypeOf(obj1,obj2)
const shallowClone = (obj)=>Object.create(Object.getPrototypeOf(obj),Object.getOwnPropertyDescriptors(obj))
shallowClone(obj1)

Object.getOwnPropertyDescriptors方法可以实现一个对象继承另一个对象。 以前的继承写法

const obj = Object.create({a:1});
obj.a; // 1

或者

const obj = Object.assign(Object.create({a:1}),{b:2});
obj;

Object.getOwnPropertyDescriptors写法

const obj = Object.create({a:1},Object.getOwnPropertyDescriptors({b:2}))

9.12 Null传导运算符

const firstName = message?.body || 'default';