JavaScript知识总结(持续更新中)

Lou.Chen
大约 36 分钟

1.let和const命令

1.1 let命令

  • let声明的变量只在所在的代码块内有效

  • var声明的变量在全局代码块内有效

  • for循环中的循环变量部分循环体内部是两个单独的作用域

    • for (let i = 0; i < 3; i++) {
        let i = 'abc';
        console.log(i);
      }
      // abc
      // abc
      // abc
      

      表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)。

let不存在变量提升

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;
}
  • 在块级作用区域内,被letconst声明的变量之前,该变量都是不可使用的。
if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}
  • 所以在对letconst声明的变量之前使用typeof是不安全的
typeof x; // ReferenceError
let x;

typeof undeclared_variable // "undefined"

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

不允许重复声明
  • 在同一个作用域内,被letconst声明的变量不能重复声明,而var声明的变量的可以重复声明
// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

// 报错
function func(arg) {
  let arg = 10;
}

// 正常
function func(a) {
  var a = 10;
  var a = 1;
}

1.2 块级作用域

var声明的变量带来的全局作用域问题

  • 内层变量可能会覆盖外层变量。

    var tmp = new Date();
    
    function f() {
      console.log(tmp);
      if (false) {
        var tmp = 'hello world';
      }
    }
    
    f(); // undefined
    

    if块var声明的变量tmp在函数f中带来的变量提升,导致输出的tmp未定义

  • 计数的循环变量泄露为全局变量

    var s = 'hello';
    
    for (var i = 0; i < s.length; i++) {
      console.log(s[i]);
    }
    
    console.log(i); // 5
    

    变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

IIFE(立即调用函数表达式)

IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScriptopen in new window 函数open in new window必须在块级作用域中使用

这是一个被称为 自执行匿名函数open in new window 的设计模式,主要包含两部分。

第一部分是包围在 圆括号运算符open in new window () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

(function () {
    statements
})();

//或者
(function () {

}());
  • 当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。

    (function () {
        var name = "Barry";
    })();
    // 无法从外部访问变量 name
    name // 抛出错误:"Uncaught ReferenceError: name is not defined"
    
  • 将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。

    var result = (function () {
        var name = "Barry";
        return name;
    })();
    // IIFE 执行后返回的结果:
    result; // "Barry"
    

示例:匿名函数将执行结果赋值给变量b,而不是将函数本身赋值给b

    let a=10;
    let b=(function (){
       return a
    })();

    //或者
    let a=10;
    let b=function (){
       return a
    }();

    // 10
    console.log(b)
块级作用域内声明函数
  • 在ES5中
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。

// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());
  • 在ES6中
    • 允许在块级作用域内声明函数。
    • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
    • 同时,函数声明还会提升到所在的块级作用域的头部。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

上面代码会解析成:行为类似于var声明的变量。上面的例子实际运行的代码如下。

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

**总结:**考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

  • // 块级作用域内部的函数声明语句,建议不要使用
    {
      let a = 'secret';
      function f() {
        return a;
      }
      f();
    }
    
    // 块级作用域内部,优先使用函数表达式
    {
      let a = 'secret';
      let f = function () {
        return a;
      };
      f();
    }
    

1.2 const命令

  • const声明一个只读的常量。一旦声明,常量的值就不能改变。

  • const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

  • 对于const来说,只声明不赋值,就会报错。

  • const的作用域与let命令相同:只在声明所在的块级作用域内有效。

  • const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

const定义的基本类型的值不能改变(数值、字符串、布尔值)

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

const定义复合类型的数据指向的指针地址不可改变,但是其数据结构可以改变。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

1.3 顶层对象属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2

var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回undefined

2.变量的解构赋值

2.1 数组的解构赋值

基本用法
let a = 1;
let b = 2;
let c = 3;

ES6写法 ==>

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

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

**模式匹配写法:**只要等号两边的模式相同,左边的变量就会被赋予对应的值

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

如果解构不成功,变量的值就等于undefined

let [foo] = [];
let [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

上面两个例子,都属于不完全解构,但是可以成功。

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

// 以下全部报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

对于 Set 结构,也可以使用数组的解构赋值。

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
默认值

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

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

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

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1];

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

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

上面最后一个表达式之所以会报错,是因为xy做默认值时,y还没有声明。

2.2 对象的解构赋值

基本用法
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序变量必须与属性同名,才能取到正确的值。

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

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

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined

如果解构失败,变量的值等于undefined

let {foo} = {bar: 'baz'};
foo // undefined

上面代码中,等号右边的对象没有foo属性,所以变量foo取不到值,所以等于undefined

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量

// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。

如果变量名与属性名不一致,必须写成下面这样:

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

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

等同于这种方式的简写==>

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

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

默认值
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不会生效。

注意点

如果要将一个已经声明的变量用于解构赋值,必须非常小心。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

// 正确的写法
let x;
({x} = {x: 1});

2.3 字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

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

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

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

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

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

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

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

2.5 常见用途

交换变量的值
let x = 1;
let y = 2;

[x, y] = [y, x];

上面代码交换变量xy的值,这样的写法不仅简洁,而且易读,语义非常清晰。

从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();
提取 JSON 数据

解构赋值对提取 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]
遍历 Map 结构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

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

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}
输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

3.字符串的扩展与新增的方法

3.1 字符串的遍历器接口for...of

字符串可以被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);
}
// "𠮷"

上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for...of循环会正确识别出这一个字符。

3.2 模板字符串``

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

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
  • 在模板字符串中需要使用反引号,则前面要用反斜杠转义。
let greeting = `\`Yo\` World!`;
  • 模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

上面代码中,所有模板字符串的空格和换行,都是被保留的,比如<ul>标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

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

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      // 传统写法为
      // 'User '
      // + user.name
      // + ' is not authorized to do '
      // + action
      // + '.'
      `User ${user.name} is not authorized to do ${action}.`);
  }

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。

let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"
  • 模板字符串之中还能调用函数。
function fn() {
  return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar
  • 如果模板字符串中的变量没有声明,将报错。
// 变量place没有声明
let msg = `Hello, ${place}`;
// 报错

由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。

`Hello ${'World'}`
// "Hello World"

模板字符串甚至还能嵌套。

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;

上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,使用方法如下。

const data = [
    { first: '<Jane>', last: 'Bond' },
    { first: 'Lars', last: '<Croft>' },
];

console.log(tmpl(data));
// <table>
//
//   <tr><td><Jane></td></tr>
//   <tr><td>Bond</td></tr>
//
//   <tr><td>Lars</td></tr>
//   <tr><td><Croft></td></tr>
//
// </table>

如果需要引用模板字符串本身,在需要时执行,可以写成函数。

let func = (name) => `Hello ${name}!`;
func('Jack') // "Hello Jack!"

上面代码中,模板字符串写成了一个函数的返回值。执行这个函数,就相当于执行这个模板字符串了。

3.3 includes(), startsWith(), endsWith()

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

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
  • endsWith('Hello',5):查找前5个字符是否以Hello开头

    • startsWith('world', 6): 从第7个字符开始,查询是否world开头
    • includes('Hello', 6):从第7个字符,查询是否包含Hello

4.函数优化

4.1 参数的默认值

//在ES6 以前,我们无法给一个函数参数设置默认值,只能采用变通写法:
function add(a, b) {
// 判断b 是否为空,为空就给默认值1
b = b || 1;
return a + b;
}
// 传一个参数
console.log(add(10));
//现在可以这么写:直接给参数写上默认值,没传就会自动使用默认值
function add2(a , b = 1) {
return a + b;
}
// 传一个参数
console.log(add2(10));

4.2 不定参数

不定参数用来表示不确定参数个数,形如,...变量名,由...加上一个具名参数标识符组成。

具名参数只能放在参数列表的最后,并且有且只有一个不定参数

function fun(...values) {
console.log(values.length)
}
fun(1, 2) //2
fun(1, 2, 3, 4) //4

4.3 箭头函数

  • 一个参数

    //以前声明一个方法
    // var print = function (obj) {
    // console.log(obj);
    // }
    // 可以简写为:
    var print = obj => console.log(obj);
    // 测试调用
    print(100);
    
  • 多个参数

    // 两个参数的情况:
    var sum = function (a, b) {
    return a + b;
    }
    // 简写为:
    //当只有一行语句,并且需要返回结果时,可以省略{} , 结果会自动返回。
    var sum2 = (a, b) => a + b;
    //测试调用
    console.log(sum2(10, 10));//20
    // 代码不止一行,可以用`{}`括起来
    var sum3 = (a, b) => {
    c = a + b;
    return c;
    };
    //测试调用
    console.log(sum3(10, 20));//30
    

4.4 箭头函数结合解构表达式

//需求,声明一个对象,hello 方法需要对象的个别属性
//以前的方式:
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
function hello(person) {
console.log("hello," + person.name)
  }

//现在的方式
var hello2 = ({ name }) => { console.log("hello," + name) };
//测试
hello2(person);

5.对象优化

5.1 新增方法

5.1.1 获取对象的所有key/value形成的数组
  • Object.keys(obj) 获取对象的所有key 形成的数组

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

  • Object.values(obj) 获取对象的所有value 形成的数组

  • Object.entries(obj)获取对象的所有key 和value 形成的二维数组。格式:[[k1,v1],[k2,v2],...]

const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
console.log(Object.keys(person));//["name", "age", "language"]
console.log(Object.values(person));//["jack", 21, Array(3)]
console.log(Object.entries(person));//[Array(2), Array(2), Array(2)]
5.1.2 浅拷贝/深拷贝
  • 浅拷贝

assign(dest, ...src) :将多个src 对象的值拷贝到dest 中。(src对象的第一层为深拷贝,第二层为浅拷贝)

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
//Object.assign 方法的第一个参数是目标对象,后面的参数都是源对象。
Object.assign(target, source1, source2);
console.log(target)//{a: 1, b: 2, c: 3}
  • 深拷贝

    • JSON.parse(JSON.stringify(obj))

      let copyObj = JSON.parse(JSON.stringify(obj))
          copyObj.a = 2
          console.log(obj, copyObj)
      

      缺点:

      该方法不能解决属性为函数,undefined,循环引用的的情况

      • 属性为为undefined、function类型的key会丢失
      • 时间对象,会变成字符串形式
      • RegExp定义的属性会变成 {}
      • NaN 类型会变成 null
      • 循环引用,导致解析报错
    • 递归方式

      function copy(obj){
              let newobj = null;
              if(typeof(obj) == 'object' && obj !== null){ 
                  newobj = obj instanceof Array? [] : {};   
                  for(var i in obj){  
                      newobj[i] = copy(obj[i])
                  }
              }else{
                  newobj = obj
              }    
            return newobj;
         }
      

      缺点:

      • 循环依赖会导致递归调用,堆栈溢出

      解决方式:Map()记录下对象中的所有对象,并与新创建的对象一一对应,即记录引用关系

      注意:如果遇到时间对象,正则等类型,需要通过new关键字去创建

          function copy(obj){
              let newobj = null;
              if(typeof(obj) == 'object' && obj !== null){ 
                  if (map.get(obj)) {
                      //打印循环依赖结果
                      newobj = map.get(obj) // 如果不想循环打印 可以设置为null
                  } else {
                      newobj = obj instanceof Array? [] : {};
                      map.set(obj, newobj)   
                      for(var i in obj){  
                          newobj[i] = copy(obj[i])
                      }
                  }
                  
              }else{
                  newobj = obj
              }    
            return newobj;
         }
      

5.2 声明对象简写

const age = 23
const name = "张三"

// 传统
const person1 = { age: age, name: name }
console.log(person1)

// ES6:属性名和属性值变量名一样,可以省略
const person2 = { age, name }
console.log(person2) //{age: 23, name: "张三"}

5.3 对象的函数属性简写

let person = {
    name: "jack",
    // 以前:
    eat: function (food) {
    console.log(this.name + "在吃" + food);
    },
    // 箭头函数版:这里拿不到this
    eat2: food => console.log(person.name + "在吃" + food),
    // 简写版:
    eat3(food) {
    console.log(this.name + "在吃" + food);
    }
}
person.eat("apple");

5.4 对象拓展运算符...

拓展运算符(...)用于取出参数对象所有可遍历属性然后拷贝到当前对象

⚠️ 第一层为深拷贝,第二层依旧为浅拷贝

如果两个对象的字段名重复,后面对象字段值会覆盖前面对象的字段值

// 1、拷贝对象(深拷贝)
let person1 = { name: "Amy", age: 15 }
let someone = { ...person1 }
console.log(someone) //{name: "Amy", age: 15}

// 2、合并对象
let age = { age: 15 }
let name = { name: "Amy" }
let person2 = { ...age, ...name } //如果两个对象的字段名重复,后面对象字段值会覆盖前面对象的字段值
console.log(person2) //{age: 15, name: "Amy"}

5.5 对象遍历

export default {
  name: "Home",
  data() {
    return {
      hrs:{
        name:'超管',
        age:99,
        city:"武汉"
      }
    }
  },
  methods:{
    changeObject() {

      for (let item in this.hrs) {
        //item为键
        console.log(this.hrs[item])
      }

      console.log("-----------------------")

      //通过Object.keys()获取索引的key,然后遍历key获取value
      Object.keys(this.hrs).forEach(key=>{
        console.log("key:",key," value:",this.hrs[key])
      })

      console.log("-----------------------")
      
    }
  }

}
</script>

6.数组方法

https://www.runoob.com/jsref/jsref-obj-array.htmlopen in new window

6.1 不会改变原数组

⚠️ 它们不会改变原始数组,而总是返回一个新数组

6.1.1 map

map() 接收一个函数,将原数组中的每个元素用这个函数处理后放入新数组返回

语法:array.map(function(currentValue,index,arr)) 数组中的每个元素都会执行这个函数

  • currentValue: 必须。当前元素的值
  • index:可选。当前元素的索引值
  • arr:可选。当前元素属于的数组对象
    let arr = ['1', '20', '-5', '3'];
    console.log(arr)
		//给数组中的每个值 *10
    // arr=arr.map((item)=>{
    //     return item*10
    // })
    // 10 200 -50 30
    arr = arr.map(item => item * 10);
    //[10, 200, -50, 30]
    console.log(arr)
6.1.2 reduce

arr.reduce(callback,[initialValue])

reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(或者上一次回调函数的返回值)当前元素值当前索引调用reduce 的数组

  • callback (执行数组中每个值的函数,包含四个参数)

    • previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))

    • currentValue (数组中当前被处理的元素)

    • index (当前元素在数组中的索引)

    • array (调用reduce 的数组)

  • initialValue (作为第一次调用callback 的第一个参数。)

    const arr = [1,20,-5,3];
    
    //没有初始值:
    console.log(arr.reduce((a,b)=>a+b));//19
    console.log(arr.reduce((a,b)=>a*b));//-300

    //指定初始值:
    console.log(arr.reduce((a,b)=>a+b,1));//20
    console.log(arr.reduce((a,b)=>a*b,0));//-0
6.1.3 filter

数组中的每个元素都会执行这个函数

array.filter(function(currentValue,index,arr))

  • currentValue: 必须。当前元素的值
  • index: 可选。当前元素的索引值
  • arr: 可选。当前元素属于的数组对象
    let arr = [1,20,-5,3,-10,20];
    //筛选除大于0的数,并返回一个新数组
    arr= arr.filter((currentValue,index,arr)=>{  //或者arr.filter(function(currentValue,index,arr){})
        return currentValue > 0;
    })
    //[1, 20, 3, 20]
    console.log(arr)
6.1.4 concat

合并多个数组并返回一个新数组,不会去重

语法:array1.concat(array2,array3,...,arrayX)

    let a = [1,1,"a","b"];
    let b = [2,2,"c","c"];
    //合并两个数组
    let c=a.concat(b)
    //[1, 1, 'a', 'b', 2, 2, 'c', 'c']
    console.log(c)

    let d=[3,3,"c","c"];
    //合并多个数组
    let e = a.concat(b, d);
    //[1, 1, 'a', 'b', 2, 2, 'c', 'c', 3, 3, 'c', 'c']
    console.log(e)
6.1.5 join

join()将数组的每一项用指定字符连接形成一个字符串。默认连接字符为 逗号

6.1.6 forEach

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。

注意: forEach() 对于空数组是不会执行回调函数的。

forEach() 本身是不支持的 continue break语句,使用 return 语句实现 continue 关键字的效果

语法:array.forEach(function(currentValue, index, arr), thisValue)

  • currentValue 必需。当前元素
  • index 可选。当前元素的索引值。
  • arr可选。当前元素所属的数组对象。
6.1.7 slice

slice() 方法可从已有的数组中返回选定的元素。

slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。

语法:array.slice(start, end) 返回一个新的数组,包含从 start(包括该元素) 到 end (不包括该元素)的 arrayObject 中的元素。

  • start: 可选。规定从何处开始选取。如果该参数为负数,则表示从原数组中的倒数第几个元素开始提取,slice(-2) 表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。
  • end: 可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。
var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
//Orange,Lemon
var citrus = fruits.slice(1,3);

var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
//Lemon,Apple
var myBest = fruits.slice(-3,-1); // 截取倒数第三个(包含)到倒数第一个(不包含)的两个元素
////Lemon,Apple,Mango
var myBest = fruits.slice(-3);  // 截取最后三个元素

6.2 改变原数组

6.2.1 push

push() 方法可向数组的末尾添加一个多个元素,并返回新的数组长度

6.2.2 pop

pop()删除并返回数组的最后一个元素,若该数组为空,则返回undefined

6.2.3 shift

shift()删除数组的第一项,并返回第一个元素的值。若该数组为空,则返回undefined

6.2.4 unshift

unshift() 向数组的开头添加一个多个元素,并返回新的数组长度

6.2.5 reverse

reverse() 将数组倒序

6.2.6 splice

splice(index,howmany,item...) 按照参数删除数组中的元素,若删除成功,则返回的是含有被删除的元素的数组, 若没有删除原始,则返回空数组

  • index [必选] 从索引0开始
  • howmany [可选] 必须为数字或者为数字字符串(可以为0),若不为数字,则不会删除任何元素。规定应该删除多少元素 如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素
  • ...item [可选] 要添加到数组的一个或者多个元素
let ids=[1,2,3,4,5]

//从索引0开始全部删除,返回[1,2,3,4,5]   原数组为[]空数组
ids.splice(0)
//从索引2开始删除1个元素,返回[3]	 原数组为[1,2,4,5]
ids.splice(2,1)
//从索引0开始删除一个元素,然后再从索引0位置添加元素100,200, 返回[1]  原数组为[100, 200, 2, 3, 4, 5 ]
ids.splice(0,1,100,200)
//从索引3开始删除一个元素,然后再从索引3位置添加元素100,200, 返回[1]  原数组为[1, 2, 3, 100, 200, 5  ]
ids.splice(3,1,100,200)

6.3 数组的遍历

<script>
export default {
  name: "Home",
  data() {
    return {
      ids: ['A', 'B', {name: "张三", age: 10}, 30],
    }
  },
  methods:{
    changeIds() {
      
			this.ids.forEach(res=>{
        //res为元素值
        console.log(res)
      })
      
      console.log("-------------------")
      
      for (let i = 0; i < this.ids.length; i++) {
        //i为索引值
        console.log(this.ids[i])
      }
      
      console.log("-------------------")
      
      for (let item in this.ids) {
        // item为索引值
        console.log(this.ids[item])
      }
      
      console.log("-------------------")
      
      for (let item of this.ids) {
        // item为元素值
        console.log(item)
      }
    }
  }
}
</script>

7.Promise

https://es6.ruanyifeng.com/?search=神拷贝&x=0&y=0#docs/promiseopen in new window

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

7.1 基本使用

注意事项:

  • 1⃣️立即执行的 resolvedPromise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务

    一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

    new Promise((resolve, reject) => {
      resolve(1);
      console.log(2);
    }).then(r => {
      console.log(r);
    });
    // 2
    // 1
    
  • 2⃣️Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

          new Promise(function(resolve, reject) {
            console.log('Promise1');
            resolve();
          }).then(function() {
            console.log('resolved1.');
          });
          
          console.log('Hi1!');
          
          new Promise(function(resolve, reject) {
            console.log('Promise2');
            resolve();
          }).then(function() {
            console.log('resolved2.');
          });
          
          console.log('Hi2!');
    //Promise1
    //Hi1!
    //Promise2
    //Hi2!
    //resolved1.
    //resolved2.
    
  • 3⃣️p1是一个 Promise,3 秒之后变为rejectedp2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

          const p1 = new Promise(function (resolve, reject) {
            setTimeout(() => reject('fail'), 3000)
          })
    
          const p2 = new Promise(function (resolve, reject) {
            setTimeout(() => resolve(p1), 1000)
          })
    
          p2.then(result => console.log(result))
              .catch(error => console.log(error))
        }
    

简单示例:

所有Promise中调用resolve才会走then方法,调用reject则会走catch方法,如下案例:所有的成功都会走then回调,失败都会走catch回调

      new Promise(function (resolve, reject){
        
        console.log('开始执行请求方法1===>')
        //模拟耗时请求
        setTimeout(() => resolve("1 >> success"), 2000)
        
      }).then(res=>{
        
        console.log("请求结果1:",res)
        return new Promise(function (resolve, reject){
          console.log('开始执行请求方法2===>')
          //模拟耗时请求
          setTimeout(() => resolve("2 >> success"), 1000)
        })
        
      }).then(res=>{
      
        console.log("请求结果2:",res)
        return new Promise(function (resolve, reject){
          console.log('开始执行请求方法3===>')
          //模拟耗时请求
          setTimeout(() => reject("3 >> fail"), 1500)
        })
        
      })
      .catch(err=>{
        console.log("err:",err)
      })
   })

开始执行请求方法1===> 请求结果1: 1 >> success 开始执行请求方法2===> 请求结果2: 2 >> success 开始执行请求方法3===> err: 3 >> fail

7.2 Promise.all

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

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

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

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

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

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,其它的成功请求也无法将结果返回给p,此时第一个被reject的实例的返回值,会传递给p的回调函数。

注意事项:

  • 若数组中的promise实例定义了自己的thencatch,那么将不会走Promise.all中的thencatch

案例:

      const p1 = new Promise((resolve, reject) => {
        console.log('开始执行p1...')
        setTimeout(() => {
          resolve('p1 success')
        }, 1000)
      })
      const p2 = new Promise((resolve, reject) => {
        console.log('开始执行p2...')
        setTimeout(() => {
          resolve('p2 success')
        }, 1500)
      })
      const p = Promise.all([p1, p2])
      p.then(res => {
        //结果为数组对象,为每个异步执行成功的resolve中传递的参数
        //p1 success
        //p2 success
        console.log('返回结果...',res)
      }).catch(err => {
        console.log('error:',err)
      })

7.3 Promise.race

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

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

上面代码中,只要p1p2p3之中有一个实例率先改变状态(无论是成功还是失败的),p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

7.4 Promise.allSettled

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

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

为了解决这个问题,ES2020 引入了Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。

Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。

注意事项

  • results的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。

    // 异步操作成功时
    {status: 'fulfilled', value: value}
    
    // 异步操作失败时
    {status: 'rejected', reason: reason}
    

案例:

      const p1 = new Promise((resolve, reject) => {
        console.log('开始执行p1...')
        setTimeout(() => {
          resolve('p1 success')
        }, 2000)
      })
      const p2 = new Promise((resolve, reject) => {
        console.log('开始执行p2...')
        setTimeout(() => {
          // reject(new Error("p2 error"))
          //或者
          reject('p2 error')
        }, 1500)
      })
      const p = Promise.allSettled([p1, p2])
      p.then(res => {
        //{status: 'fulfilled', value: 'p1 success'}
        //{status: 'rejected', reason: 'p2 error'}
        console.log('返回结果...',res)
        //过滤处成功的请求
        const success=res.filter(item=>item.status=="fulfilled")
        console.log(success)
        //过滤处失败的请求
        const error=res.filter(item=>item.status=="rejected")
        console.log(error)
      })

7.5 Promise.any

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

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

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

Promise.any()必须等到所有参数 Promise 变成rejected状态才会走到catch方法结束。

注意事项:

  • 只要有任一参数的promise变为fulfilled则就会执行pthen方法,当所有参数都执行完rejectp才会执行catch方法

案例:

			const p1 = new Promise((resolve, reject) => {
        console.log('开始执行p1...')
        setTimeout(() => {
          resolve('p1 success')
        }, 1000)
      })
      const p2 = new Promise((resolve, reject) => {
        console.log('开始执行p2...')
        setTimeout(() => {
          reject('p2 error')
        }, 1500)
      })
      const p = Promise.any([p1, p2])
      p.then(res => {
        console.log('返回结果...',res)
      }).catch(err=>{
        console.log("err:",err)
      })

8.async/await

8.1 基本使用

async 函数是一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样,就定义了一个 async 函数

async function test(){
  //....
}

注意事项

  • async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数

    • 没有显式return,相当于return Promise.resolve(undefined)
    • returnPromise的数据data,相当于return Promise.resolve(data)
    • return Promise, 会得到Promise对象本身

    以下函数返回的都是Promise对象

    	 async function testA(){}
       async function testB(){
           return Promise.resolve("22");
        }
        async function testC(){
            return "22";
        }
    
  • async总是返回Promise,因此,其后面可以直接调用then方法。函数内部return返回的值,会成为then回调函数的参数,函数内部抛出的错误(需显式返回reject()),会被then的第二个函数或catch方法捕获到

  • await必须在async函数内部才能调用,调用时,在async内部会等到await调用完成,才会继续后续操作

    • 如果await等待的不是一个Promise对象,那await表达式的运算结果就是它等到的东西。

    • 如果await等待的是一个Promise对象,那么它会阻塞后面的代码,等待Promise对象返回resolve作为await表达式的运算结果,若返回reject,则会报错或者由调用者的catch

      捕获或者then的第二个回调参数捕获

案例:

  • 只有async函数中使用Promise.reject()方法抛出错误才会被调用者捕获。若不在async函数中显示执行返回reject,则默认执行Promise.resolve(),表示成功,即永远不会失败
  • 若在async函数中的await等待执行的函数抛出Promise.reject()函数错误,那么后面所有的语句将不会执行,直接由调用者捕获错误
      function test(n) {
        return new Promise((reslove, reject) => {
          setTimeout(() => reslove(n + 100), 1000)
        })
      }

      async function step1(n) {
        console.log("first step...")
        return test(n)
      }

      async function step2(n) {
        console.log("second step...")
        return test(n)
      }

      async function step2(n) {
        console.log("second step...")
        return test(n)
      }

      async function step3() {
        console.log("third step...")
        return Promise.reject('third err...')
      }

      async function execute() {
        console.log("start execute...")
        //执行第一个异步方法
        const firstStepResult = await step1(100)
        console.log("first step result:", firstStepResult)
        //执行第二个异步方法
        const secondStepResult = await step1(firstStepResult)
        console.log("second step result:",secondStepResult)
        //执行第三个异步方法 (有reject错误抛出)
        await step3() //被execute的catch方法捕获,或被then的第二参数的回调函数捕获
      }

      execute().then(resolve => {
        console.log("success:",resolve)
      }).catch(err => {
            console.log("err:", err)
      })

//start execute...
//first step...
//first step result: 200
//first step...
//second step result: 300
//third step...
//err: third err...