JavaScript知识总结(持续更新中)
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;
}
- 在块级作用区域内,被
let
和const
声明的变量之前,该变量都是不可使用的。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
- 所以在对
let
和const
声明的变量之前使用typeof
是不安全的
typeof x; // ReferenceError
let x;
typeof undeclared_variable // "undefined"
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
- 在同一个作用域内,被
let
和const
声明的变量不能重复声明,而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( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。必须在块级作用域中使用
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。
第一部分是包围在 圆括号运算符
()
里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 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
上面代码中,全局变量a
由var
命令声明,所以它是顶层对象的属性;全局变量b
由let
命令声明,所以它不是顶层对象的属性,返回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
上面最后一个表达式之所以会报错,是因为x
用y
做默认值时,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
,因为null
与undefined
不严格相等,所以是个有效的赋值,导致默认值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
都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
2.5 常见用途
交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
上面代码交换变量x
和y
的值,这样的写法不仅简洁,而且易读,语义非常清晰。
从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组
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.html
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/promise
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⃣️立即执行的
resolved
的Promise
是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务一般来说,调用
resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上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 秒之后变为rejected
。p2
的状态在 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()
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,其它的成功请求
也无法将结果返回给p,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
注意事项:
- 若数组中的
promise
实例定义了自己的then
和catch
,那么将不会走Promise.all
中的then
和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(() => {
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]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态(无论是成功还是失败的),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
则就会执行p
的then
方法,当所有参数都执行完reject
,p
才会执行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)
return
非Promise
的数据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...