0%

两种表达式

1.对象字面量
以斜杠/表示开始和结束.

1
let regx = /xyz/;

2.使用RegExp构造函数.

1
let regx = new RegExp("xyz");

两种方法等价,区别在于第一种在引擎编译代码时,新建正则表达式.
第二种在运行时新建正则表达式.

第二参数

1
2
3
let regx = /xyz/g;
//等价于
let regx = new RegExp("xyz", "g");

实例方法

RegExp.prototype.test()

返回一个布尔值,表示当前模式是否能够匹配参数表达式.

1
/cat/.test("cats and dogs"); // true

验证字符串中是否含有cat.
如果含有g修饰符,则每一次test方法都从上一次结束的位置开始向后匹配.

1
2
3
4
5
6
let r = /z/g;
let s = "_z_z";

r.test(s); //true,(从第一个_z前面开始)
r.test(s); //true,(从第二个_z前面开始)
r.test(s); //false,(从第二个_z后面开始)

g修饰符表示全局搜索.
如果正则是一个空字符串,则会匹配所有的字符串.

1
new RegExp("").test("abc"); //true

RegExp.prototype.exec()

返回匹配结果.如果发现匹配,即返回一个数组.成员是匹配成功的子字符串,否则返回null.

1
2
3
4
5
6
let s = "_X_X";
let r1 = /x/;
let r2 = /y/;

r1.exec(s); //["x"]
r2.exec(s); // null

组匹配

正则如果包含(),则返回的数组包含多个成员.
第一个成员是整个匹配成功的结果.第二个成员对应第一个括号,第二个成员对应第二个括号.

1
2
3
let s = "_x_x";
let r = /_(x)/;
r.exec(s); //["_x", "x"]

Srting.prototype.match()

字符串实例对象的match方法对字符串进行正则匹配,返回匹配结果.

1
2
3
4
5
let s = "_x_x";
let r1 = /x/;
let r2 = /y/;
s.match(r1); //["x"}
s.match(r2); //

字符串的match方法与正则对象的exec非常类似.
匹配成功返回一个数组,匹配失败返回 null.
如果正则表达式带有g修饰符,则该方法与exec不同,会一次性返回所有匹配成功的结果.

1
2
3
4
let s = "abba";
let r = /a/g;
s.match(r); //["a","a"]
s.exec(r); //["a"]

返回第一个满足匹配结果在整个字符串的位置.如果没有返回-1.

1
'_x_x'search(/x/); //1

String.prototype.replace()

字符串对象的replace可以替换匹配的值.它接收两个参数,第一个是正则表达式.表示搜索模式.
第二个是替换内容.

1
str.replace(search, replacement);

如果不加g,就替换匹配成功的第一个值,否则替换所有值.

1
2
3
4
let str = "aaa";
str.replace(a, b); //"baa"
str.replace(/a/, b); //"baa"
str.replace(/a/g, b); //"bbb"

使用repalce消除字符串首尾两端的空格.

1
2
3
let str = "     #id div.class   ";
str.replace(/^\s+|\s+$/g, "");
//#id div.class

replace第二个参数可以使用美元符号$,用来指代所替换的内容.

$&:匹配的子字符串。
$`:匹配结果前面的文本。
$’:匹配结果后面的文本。
$n:匹配成功的第 n 组内容,n 是从 1 开始的自然数。
$$:指代美元符号$。

1
2
3
let str = "abc";
str.replace("b", "[$`-$&-$']");
//"a[a-b-c]c"

作为 replace 方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。

String.prototype.split()

该方法接收两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数.

1
2
3
4
5
6
7
8
9
10
11
// 非正则分隔
"a, b,c, d".split(",");
// [ 'a', ' b', 'c', ' d' ]

// 正则分隔,去除多余的空格
"a, b,c, d".split(/, */);
// [ 'a', 'b', 'c', 'd' ]

// 指定返回数组的最大成员
"a, b,c, d".split(/, */, 2);
//[ 'a', 'b' ]
1
2
3
4
5
6
7
// 例一
"aaa*a*".split(/a*/);
// [ '', '*', '*' ]

// 例二
"aaa**a*".split(/a*/);
// ["", "*", "*", "*"]

上面代码的分割规则是 0 次或多次的 a,由于正则默认是贪婪匹配,所以例一的第一个分隔符是 aaa,第二个分割符是 a,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是 aaa,第二个分隔符是 0 个 a(即空字符),第三个分隔符是 a,所以将字符串分成四个部分。

如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。

1
2
"aaa*a*".split(/(a*)/);
// [ '', 'aaa', '*', 'a', '*' ]

匹配规则

点字符

点字符匹配除回车\r,换行\n,行分隔符\u2028,段分隔符\2029以外的所有字符

1
/c.t/;

上面代码中,c.t 匹配 c 和 t 之间包含任意一个字符的情况,只要这三个字符在同一行,比如 cat、c2t、c-t 等等,但是不匹配 coot。

位置字符

提示字符所处的位置.

1
2
3
4
5
6
7
8
9
10
^ 表示字符串的开始位置
$ 表示字符串的结束位置
\b 单词边界的位置
\B 非单词边界的位置
x(?=p) x后面的字符与p匹配时匹配x,即匹配p前面的位置
比如对于/Jack(?=Sprat)/,“Jack”在跟有“Sprat”的情况下才会得到匹配.
x(?!p) x后面的字符不与p匹配时匹配x,即匹配非p前面的位置
(?<=p)x x前面的字符与p匹配时匹配x,即匹配p后面的位置
例如,对于/(?<=Jack)Sprat/,“Sprat”紧随“Jack”时才会得到匹配。
(?<!p)x x前面的字符不与p匹配时匹配x,即匹配非p后面的位置

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在字符串首尾(即^和$)处插入“#”
"javascript css html".replace(/^|$/g, "#"); // "#javascript css html#"

// 在单词边界(即\b)处插入“#”
"javascript css html".replace(/\b/g, "#"); // "#javascript# #css# #html#"
// 非单词边界(即\B)处插入“#”
"javascript css html".replace(/\B/g, "#"); // "j#a#v#a#s#c#r#i#p#t c#s#s h#t#m#l"

// 在空格前的位置插入“#”
"javascript css html".replace(/(?=\s)/g, "#"); // "javascript# css# html"
// 在非空格前的位置插入“#”
"javascript css html".replace(/(?!\s)/g, "#"); // "#j#a#v#a#s#c#r#i#p#t #c#s#s #h#t#m#l#"

// 在空格后的位置插入“#”
"javascript css html".replace(/(?<=\s)/g, "#"); // "javascript #css #html"
// 在非空格后的位置插入“#”
"javascript css html".replace(/(?<!\s)/g, "#"); // "#j#a#v#a#s#c#r#i#p#t# c#s#s# h#t#m#l#"

选择符(|)

竖线符号在正则表达式中表示”或关系”

转义符

正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+,就要写成\+
正则表达式中,需要反斜杠转义的,一共有 12 个字符:^.[$()|*+?{\\。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。

特殊字符

\cX 表示Ctrl-[X],其中的 X 是 A-Z 之中任一个英文字母,用来匹配控制字符。
[\b] 匹配退格键(U+0008),不要与\b混淆。
\n 匹配换行键。
\r 匹配回车键。
\t 匹配制表符 tab(U+0009)。
\v 匹配垂直制表符(U+000B)。
\f 匹配换页符(U+000C)。
\0 匹配 null 字符(U+0000)。
\xhh 匹配一个以两位十六进制数(\x00-\xFF)表示的字符。
\uhhhh 匹配一个以四位十六进制数(\u0000-\uFFFF)表示的 Unicode 字符。

匹配带千分位的金额,并以正常字符串输出

1
2
num = s.match(/\d*(\,\d+)*(\.\d+)?/g)[0].replace(/\,/gi, "");
//匹配带千分位的金额,并以正常字符串输出

量词

贪婪模式

尽可能多的匹配

1
2
3
4
5
6
x{m,n}	x出现的次数为m-n次
x{m,} x至少出现m次
x{m} x出现m次,相当于{m,m}
x? x出现01次,相当于{0,1}
x+ x出现1或多次,相当于{1,}
x* x出现0或多次,相当于{0,}

在 x{m,n}中,x 表示待匹配的项,可以是一个字符或一个整体,m 为 0 或者正整数,n 为正整数,且 n>m。

例子 1:/a{2,4}bc/中 a{2,4}指的是字符串中 a 出现的次数是 2-4 次

1
2
3
4
const regExp = /a{2,4}bc/;
regExp.test("aaabc"); // true
regExp.test("aabc"); // true
regExp.test("abc"); // false

非贪婪模式

尽可能少的匹配

1
2
3
4
5
x{m,n}?	x出现的次数m-n次
x{m,}? x至少出现m次
x?? x出现01次,相当于{0,1}
x+? x出现1或多次,相当于{1,}
x*? x出现0或多次,相当于{0,}

上面我们讲的重复字符都是尽可能多地匹配,这种我们称为贪婪的匹配,同时我们还有非贪婪的匹配(也可以叫惰性匹配),它是尽可能少的匹配。
语法:在表示量词后跟随一个“?”。

例子 1:使用”aaa”作为匹配字符串,用正则表达式/a+/去匹配,匹配的结果是”aaa”,用/a+?/匹配的结果是”a”。

1
2
3
4
5
"aaa".match(/a+/);
// ["aaa", index: 0, input: "aaa", groups: undefined]

"aaa".match(/a+?/);
// ["a", index: 0, input: "aaa", groups: undefined]

修饰符

1
2
3
4
5
6
i 不区分大小写
g 全局匹配,查找所有的匹配项,没有 g 修饰符的话匹配到第一个就停止了
m 多行匹配模式,^/$匹配一行或者字符串的开头/结尾
s 允许.匹配换行符
u 使用 unicode 码的模式进行匹配。
y 执行“粘性(sticky)”搜索,匹配从目标字符串的当前位置开始。

圆括号的作用

1
2
3
4
5
6
7
8
9
(x) 捕获组:匹配 x 并将括号内的正则表达式定义为一个子表达式,
供后面反向引用使用或者获取子表达式匹配结果。
使用结果元素的索引 ([1], ..., [n]) 或
从预定义的  RegExp  对象的属性 ($1, ..., $9)来获取。
捕获组会带来性能损失,如果你不需要存储匹配项或者你的括号只是分组作用,
可以选择下面的非捕获组。
(?:x) 非捕获组:匹配 x 并且不定义子表达式,只用括号最原始的功能
\n 反向引用:n 是一个正整数,引用之前出现的分组,\1 表示正则中与第一个左括号相匹配的子表达式
(?<Name>x) 具名捕获组:匹配 x 并将其存储在返回的匹配项的 groups 属性中
1
2
/^abc{3}$/  //abccc
/^(abc){3}$/ abcabcabc //abc 重复 3 次

数据结构 Set 和 Map

整体概述:

Set是一种类似数组的数据结构,区别在于其存储的成员都是不重复的,由此带来了它的一个应用就是:去重。Set通过new关键字实例化,入参可以是数组 or 类数组的对象。

值得注意的是:在Set中,只能存储一个NaN,这说明在 Set 数据结构中,NaN等于NaN

Set实例的方法:
操作方法 add()、delete()、has()和 clear();
遍历方法:keys()、values()、entries()和 forEach();扩展运算符…、
数组方法 map()、filter()方法也可以用于 Set 结构。
由此它可以很方便的实现数组的交、并、差集。

WeakSet类似于 Set,主要区别在于 1.成员只能是对象类型; 2.对象都是弱引用(如果其他对象都不再引用该对象,垃圾回收机制会自动回收该对象所占的内存,不可预测何时会发生,故 WeakSet 不可被遍历)

JavaScript 对象 Object 都是键值 K-V 对的集合,但 K 取值只能是字符串和 Symbol,Map 也是 K-V 的集合,然而其 K 可以取任意类型。
如果需要键值对的集合,Map 比 Object 更适合。Map 通过 new 关键字实例化。

Map实例的方法:set()、get()、has()、delete()和 clear();
遍历方法同 Set。

Map与其它数据结构的互相转换:Map <—> 数组| Map <—> 对象| Map <—> JSON。

WeakMap类似于Map,主要区别在于: 1.只接受对象作为键名; 2.键名所指向的对象不计入垃圾回收机制。

Set

Set 是一个类似数组,但是与数组不同,它具有唯一性.里面的元素都是不重复的,本身是一个构造函数.

1
2
3
4
5
6
7
8
9
10
11
const SetArr = new Set();

[1,1,2,2,3,3].forEach(item=>SetArr.add(item));
console.log(SetArr);

=>
0: 1
1: 2
2: 3
size: 3
__proto__: Set

实例属性

  • constructor: 构造函数,就是 Set 函数
  • size: 返回的是 Set 实例的长度

实例方法

  • add(): 往 Set 里添加值,返回 Set 本身
  • delete(): 删除某个值,返回布尔值判断是否成功
  • has(value): 判断是否有 value,返回布尔值
  • clear(): 清除 Set 中所有值,无返回值

遍历方法

  • forEach(): 使用回调函数遍历元素
  • entries(): 返回键值对的遍历器,用于遍历[键名,键值]组成的数组
  • values(): 返回键值遍历器,用于遍历所有键值
  • keys(): 返回键名遍历器,用于遍历所有键名

    由于 Set 结构是只有键值的结构,所有 keys 方法与 values 方法返回一致。

forEach

1
2
3
let s = new Set([1, 2, 3, 4, 5]);
s.forEach((item) => console.log(item + 1));
//2 3 4 5 6

entries

1
2
3
4
5
6
7
let s = new Set(["a", "b", "c"]);
for (let item of s.entries()) {
console.log(item);
}
//(2) ["a", "a"]
//(2) ["b", "b"]
//(2) ["c", "c"]

values

1
2
3
4
5
let s = new Set(["a", "b", "c"]);
for (let item of s.values()) {
console.log(item);
}
//a b c

keys

1
2
3
4
5
let s = new Set(["a", "b", "c"]);
for (let item of s.keys()) {
console.log(item);
}
//a b c

使用 set 实现交并差

1
2
3
4
5
6
7
8
9
let a = new Set([1, 2, 3, 4]);
let b = new Set([3, 4, 5, 6]);

//交集
let value = new Set([...a].filter((item) => b.has(item)));
//并集
let value2 = new Set([...a], [...b]);
//差集
let value3 = new Set([...a].filter((item) => !b.has(item)));

WeakSet

一个弱的 Set 结构,弱的体现在WeakSet的成员只能是对象.
WeakSet的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用.
WeakSet没有 size 属性,无法遍历.只有实例方法:

  • add(): 向WeakSet添加新成员
  • delete(): 删除WeakSet的成员
  • has(): 判断WeakSet是否包含某个元素

Map

Set 结构是没有键只有值,而 Map 结构是键值的组合.也就可以使用各种数据类型作为键(包括对象).

1
2
3
4
5
let map = new Map();
let obj = { name: "alan", age: 20 };
map.set(obj, "这是一些描述");
map.get(obj);
console.log(map);

实例属性

  • constructor: Map 构造函数
  • size: 返回的是 Map 元素的长度

实例方法

  • set(key, value): 设置 key 所对应的 value 值
  • get(key): 获取 key 所对应的 value 值,若无则返回 undefined
  • delete(): 删除某个值,返回布尔值判断是否成功
  • has(value): 判断是否有 value,返回布尔值
  • clear(): 清除 Set 中所有值,无返回值

遍历方法

  • forEach(): 使用回调函数遍历元素
  • entries(): 返回键值对的遍历器,用于遍历[键名,键值]组成的数组
  • values(): 返回键值遍历器,用于遍历所有键值
  • keys(): 返回键名遍历器,用于遍历所有键名

keys

1
2
3
4
5
6
7
8
9
let person = new Map([
["name", "alan"],
["age", "20"],
]);
for (let item of person.keys()) {
console.log(item);
}
//name
//age

values

1
2
3
4
5
6
7
8
9
let person = new Map([
["name", "alan"],
["age", "20"],
]);
for (let item of person.values()) {
console.log(item);
}
//alan
//20

与其他数据结构转换

  • Map 转数组
1
2
3
4
5
let person = new Map([
["name", "alan"],
["age", "20"],
]);
console.log(...person);
  • 数组转 Map
1
2
3
4
5
let arr = [
["name", "alan"],
["age", "20"],
];
let m = new Map(arr);
  • Map 转对象
1
2
3
4
5
6
7
8
9
10
11
12
13
let person = new Map([
["name", "alan"],
["age", "20"],
]);
function swap(map) {
let obj = Object.create(null);
for (let [key, value] of map) {
obj[key] = value;
}
return obj;
}
swap(person);
//{name: "allan", age: "20"}
  • 对象转 Map
1
2
3
4
5
6
7
8
let person = { name: "allen", age: "20" };
function swap(obj) {
for (let key of Object.keys(obj)) {
map.set(key, obj[key]);
}
return map;
}
swap(person);

WeakMap

WeakMap 与 Map 一样可以生成键值对的集合,但是也有不同的地方,主要是有以下的两点:

  • WeakMap 只接受对象作为键名(不包括 null)
  • ct

与 WeakSet 相似,WeakMap 也是没有遍历的操作,也没有 size 属性,没有办法列出所有键名(由于垃圾回收机制的运行),也不能清空。

实例方法

  • get():
  • set():
  • delete(): 删除 WeakMap 的成员
  • has(): 判断 WeakMap 是否包含某个元素

Array.from

将类数组对象转化为数组.
第二个参数可以对传入值进行处理.

模块化

方式有立即执行函数,AMD,CMD,CommonJS,ES6 模块化.
其中最常用的是 CommonJS 和 ES6 模块化

区别

  • CommonJS 即使用require(),module.exports,通常在 Node 中使用.支持动态导入.属于同步导入.
  • ES6 模块化即使用import,export default.通常用于浏览器,属于异步导入.
  • CommonJS 在导出时是值拷贝,就算导出的值变了,导入的也不会变.如果想更新,必须重新导入.ES6 模块化采用实时绑定,导出导入都指向同一个地址,所以导入变化导出也会变化.
  • ES6 模块会编译成require/exports来执行.
1
2
3
4
5
6
7
8
9
10
11
12
13
// AMD
define(["./a", "./b"], function (a, b) {
// 加载模块完毕可以使用
a.do();
b.do();
});
// CMD
define(function (require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require("./a");
a.doSomething();
});

Commonjs

1
2
3
4
5
6
7
8
9
10
// a.js
module.exports = {
a: 1,
};
// or
exports.a = 1;

// b.js
var module = require("./a.js");
module.a; // -> log 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var module = require("./a.js");
module.a;
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1,
};
// module 基本实现
var module = {
id: "xxxx", // 我总得知道怎么去找到他吧
exports: {}, // exports 就是个空对象
};
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports;
var load = function (module) {
// 导出的东西
var a = 1;
module.exports = a;
return module.exports;
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over

另外虽然 exports 和 module.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exports 和 module.exports 享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效。

箭头函数

  1. 箭头函数继承而来的 this 指向永远不变.

使用call,apply,bind都无法改变箭头函数 this 指向.

  1. 箭头函数不能作为构造函数使用.因为它没有自己的 this
  2. 箭头函数没有自己的arguments对象.

在箭头函数中访问 arguments 实际上获得的是外层局部(函数)执行环境中的值.
如果想访问箭头函数中的参数,可以使用rest参数.

  1. 箭头函数没有原型prototype
  2. 箭头函数不能用作Generator函数,不能使用 yeild 关键字

类和继承

ES6 的类,完全可以看作构造函数的另一种写法。
类的数据类型就是函数,类本身就指向构造函数。

1
2
3
4
5
6
class Point {
// ...
}

typeof Point; // "function"
Point === Point.prototype.constructor; // true

类的所有方法都定义在类的 prototype 属性上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};

constructor 关键字

constructor()方法就是构造方法,而this关键字则代表实例对象。
constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有 constructor()方法,如果没有显式定义,一个空的constructor()`方法会被默认添加。

extends 关键字

作用:类继承
底层原理: 替换原型继承(不会覆盖子构造函数的原型)
extends 关键字替换原型的原型:s1.__proto__.__proto__ = Person.prototype

super 关键字

用于调用父类的方法.
子类中如果想使用constructor,必须使用super关键字调用父类的方法.
super这个关键字,既可以当作函数使用,也可以当作对象使用

在子类的构造函数中,只有调用 super()之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有 super()方法才能让子类实例继承父类。
如果子类没有定义 constructor()方法,这个方法会默认添加,并且里面会调用 super()。也就是说,不管有没有显式定义,任何一个子类都有 constructor()方法。

当做函数

1
2
3
4
5
6
7
class A {}

class B extends A {
constructor() {
super();
}
}

注意,super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B 的实例,因此 super()在这里相当于 A.prototype.constructor.call(this)。

当做对象

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

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}

let b = new B();

上面代码中,子类 B 当中的 super.p(),就是将 super 当作一个对象使用。这时,super 在普通方法之中,指向 A.prototype,所以 super.p()就相当于 A.prototype.p()。

注意,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Person {
// private 私有,仅在类内使用
private name:string = 'Tom';
// public 公共,内外都可以使用
public age: number = 10;
// protected 可在类内和继承的子类中调用
protected sex: string;
getName() {
return 'Name'
}
//constructor中的会在new一个实例时调用,接收传入的参数
constructor(name: string) {
this.name = name
}
}

const person = new Person('dell');//此时调用constructor中的方法
console.log(person.name);

class Teacher extends Person {
getTeacherName() {
return 'teacher'
}
getName(){
//super关键字表示父类
return super.getName()
}
constructor(age: number){
//子类中的constructor要想使用,需要调用父类的constructor
//super()就表示父类的constructor
super('dell');
}
}
const person = new Teacher(123);//此时调用constructor中的方法
console.log(person.name); // 子类继承了父类,并且super()传入'dell',则打印dell
console.log(person.age); // 子类继承了父类,虽然传参123,
// 但是子类的构造函数内没有对应的this赋值给age,则继承父类的age,父类的原型对象上age是10

类中的 getter 和 setter

私有属性无法在外部调用,可以利用getter间接调用私有属性.使用setter间接设置私有属性.

1
2
3
4
5
6
7
8
9
10
11
class Person {
constructor(private _name: string) {}
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
}
}
const person = new Person("dell");
person.name = "dell";

单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//static表示挂载在类上,而不是类的实例上
class Demo {
private static instance: Demo;
private constructor() {}

static getInstance() {
if (!instance) {
this.instance = new Demo();
}
return this.instance;
}
}

const demo1 = Demo.getInstance();
const demo2 = Demo.getInstance();

抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用abstract 关键字抽象类,将公共方法属性抽离出来
abstruct class Geom {
width: number;
getType() {
return 'Geom'
}
// 这里写abstract的话,子类中必须声明此方法
abstract getArea(): number;
}

class Circle extends Geom {
getArea() {
return '123'
}

Proxy

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
Proxy 内置一系列陷阱,用于创建一个对象的代理,从而实现基本操作的拦截和自定义.

简单来说, 通过Proxy创建对于原始对象的代理对象,从而在代理对象中使用Reflect达到对JS操作的拦截.
1
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
1
2
3
4
5
6
7
8
9
10
11
const obj = {
name: 'Tom'
}
const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log('劫持的对象名', target)
console.log('劫持的属性名', key)
return target[key]
}
})
proxy.name // "Tom"
上面就是通过Proxy创建了一个代理对象,同时在Proxy内部声明了一个get陷阱. 当我们访问`proxy.name`时实际触发了对应的get陷阱.执行陷阱中的逻辑. `target`就是劫持的源对象`obj`, `key`就是劫持的获取的name值,注意getter函数取值时才会触发. `receiver`表示代理的对象proxy或者继承Proxy的对象.也就是谁调用了函数,就是指的谁.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const parent = {
name: 'parent',
get value() {
return this.name;
},
};

const handler = {
get(target, key, receiver) {
return Reflect.get(target, key);
// 这里相当于 return target[key]
},
};

const proxy = new Proxy(parent, handler);

const obj = {
name: 'obj',
};

// 设置 obj 继承与 parent 的代理对象 proxy
Object.setPrototypeOf(obj, proxy);

console.log(obj.value); // parent
// 这里因为返回的是源对象的 value,所以是父级的,想要是继承的,需要加上 receiver

const handler = {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
// 这里相当于 return target[key].call(receiver)
},
};

````

- Proxy中接受的Receiver形参表示代理对象本身或者继承自代理对象的对象
- Reflect中传递的Receiver实参表示修改执行原始操作时的this指向.(实际上就是指向调用者).

接下来我们通过 Proxy 来实现一个数据响应式
```javascript
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value, receiver)
}
}
return new Proxy(obj, handler)
}

let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
````

在上述代码中,我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

# Reflect

Reflect 提供拦截 JS 操作的方法.并非一个构造函数,不能用 new 进行调用.
它的所有方法和属性都是静态的.

```javascript
Reflect.get(target, key, receiver);
// 也就是Reflect将get操作转发给target,获取key的值,相当于target[key]

虽然也可以直接使用target[key],但是 Reflect 可以用 receiver,更强大.

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 实现功能
let array = [1, 2, 3];

array[-1]; // 3,最后一个元素
array[-2]; // 2,从末尾开始向前移动一步
array[-3]; // 1,从末尾开始向前移动两步

array = new Proxy(array, {
get(target, key) {
// key 是字符串
key = Number(key);
if (Number(key) < 0) {
key = key + target.length;
}
return Reflect.get(target, key);
},
});

深层遍历

1
2
3
4
5
6
7
8
9
const handler = {
get(target, key){
let res = Reflect.get(target.key)
retrun typeof res === 'object' ? new Proxy(res,handler) : res
},
set(target, key, val){
return Reflect.set(target, key, val)
}
}

Generator

Generator 即生成器,它会返回一个迭代器.Generator 最大的特点就是可以控制函数的执行。可以作为异步解决方案.

1
2
3
4
5
6
7
8
9
function* foo(x) {
let y = 2 * (yield x + 1);
let z = yield y / 3;
return x + y + z;
}
let it = foo(5);
console.log(it.next()); // => {value: 6, done: false}
console.log(it.next(12)); // => {value: 8, done: false}
console.log(it.next(13)); // => {value: 42, done: true}

Generator 函数需要手动调用next执行,在遇到yield会暂停,返回一个对象,包括执行结果和是否执行完毕.继续调用next会从之前暂停的地方开始,如果传入参数,会覆盖之前的结果.到下一个yield结束.
分析上面的代码:

  • 第一次执行next,参数是 5,遇到yield暂停,返回值为 5+1 => 6.
  • 第二次执行next,next有参数 12, 覆盖上一次yield处的值,就是y = 2*12,要在这一次的yield处暂停就要 24/3,得到 8
  • 最后一次执行next,z 就被赋值为 13,结合第二次 y 是 24,第一次 x 是 5,最后返回 5+13+24=42.

Promise

Promise 是异步问题同步化解决方案.
Promise 本身不是异步,它是个构造函数.
Promise 中的函数是同步的.
Promise 有一个参数,即excutor执行器,它有两个参数,resolvereject.
excutor是同步执行,而then是异步调用的.
如果想连续then需要上一个promise中 return 一个新的promise.
Promise 对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止.

状态

promise 内部在resolve之前的代码处于pending状态,之后的是fulfilled.
promise 在catch中如果没有抛出错误,则会返回fufilled

链式调用

状态固化后,就不再捕获错误了.比如 resolve()返回后面有报错的内容,但是 catch 就不再捕获了.
案例:
当 promise 作为参数传递到另一个 promise 中,这个第二个 promise 中的状态就会失效.依赖于第一个 promise.

1
2
3
4
5
6
7
8
const p1 = new Promise((reslove,reject)=>{
setTimeout(()=>{reject(new Error('fail')},3000)
})
const p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{resolve(p1)},1000)
})
p2.then(result=>console.log(result))
.catch(err=>console.log(err))

Promise.all()如果三个内容都失败,只返回第一个失败的信息.如果只有一个出错,也只会返回一个.而且不返回正常的.
Promise.race()是谁先完成返回谁,无论是成功还是失败.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
promise
.then((value) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Error");
}, 300);
});
})
.then(
(value) => {
console.log(value);
},
(reason) => {
console.log(reason);
} // 'Error'
// 默认return 的是undefined
)
.then(
//上一次调用只有失败的状态,而那里默认返回的是undefined,
//这里在onFulfilled的这里捕获的是上一次失败的默认return的undefined
(value) => {
console.log(value);
}, // 'undefined'
(reason) => {
console.log(reason);
}
);
1
2
3
4
5
6
7
8
9
10
// then中throw new Error()的情况
.then(
(value)=>{throw new Error('Error')
)
.then(
// 上面抛出的是error,所以要走onRejectd,也就是第二个函数
(value)=>{console.log(value)},
(reason)=>{console.log(reason)} // 'Error'
)

缺点

其实它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。

如何中断 promise

插入一个 pending 状态的 promise.

catch

catch 在 promise 的源码层面就是一个 then,如果 catch 中 return 的有值,可以在 catch 后跟 then.
既有 then 中的失败回调,又有 catch 的情况,会走最近的那个.

1
2
3
4
5
6
7
8
9
10
11
12
13
const p1 = Promise.resolve();
p1.then(() => {
console.log(1);
throw new Error();
})
.catch(() => {
console.log(2);
})
.then(() => {
console.log(3);
});
// 输出1,2,3
// catch后没有抛出错,则返回的是resolved状态,可以继续then

案例

使用 promise 加载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function loadIMg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement("img");
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(new Error("加载失败"));
};
img.src = src;
});
}
const url1 =
"https://i1.hdslb.com/bfs/face/f4d60f852eb1a85696447838c90a94acad31b7ae.jpg@160w_160h_1c_1s.webp";
const url2 =
"https://i0.hdslb.com/bfs/face/a8ef30d6688d0b532bd20baa160417deae8f386d.jpg@240w_240h_1c_1s.webp";
// 连续加载图片
loadImg(url1)
.then((img) => {
console.log(img);
return loadImg(url2);
})
.then((img2) => {
console.log(img2);
});

语法糖

Promise.resolve()
Promise.reject()

Promise.all

如果所有都正确执行,会按顺序返回.有一个失败,就失败.

Async/await

一个函数前加了async,它就会返回 Promise.它的返回值会被Promise.resolve()包裹.

await只能配套async使用

缺点

因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
如果代码没有依赖性的话,完全可以使用 Promise.all 的方式

async/await 和 promise 的关系

await 是等待 promise 的返回,await 做不了 promise 返回状态的工作.
await 对应Promise.then成功的情况.
await 后如果是函数,则会把函数返回的结果用Promise.then()返回.
async 中的Promise.reject()需要使用try/catch进行捕获错误.
async 中的函数内部属于同步代码,在遇到 await 之前都可以先执行.

1
2
3
4
5
6
7
8
9
let a = 0;
let b = async () => {
a = a + (await 10);
console.log("2", a); // -> '2' 10
};
b();
a++;
console.log("1", a); // -> '1' 1
// 先得到'1' 1, 后得到'2' 10
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator。

365 面试

  1. 什么是闭包?带来的问题?如何解决?

能读取其他函数内部变量的函数.
内存泄漏
let fn() = null

  1. JS 的基本类型和引用类型.

基本类型: String,number,Boolean,undefined,null,symbol.
引用类型: Object.(又可以分为对象,数组,函数,正则)

  1. let a = {name:123}, let b= a, b.name = 456,console.log(a.name)?

b.age = 13.console.log(a.age)?
输出 456 和 13.
原因: 对象是引用类型,将值保存在堆内存中.对象的值只是将指针指向堆内存中的数据.
当堆内存中数据改变,自然指向的值也会改变.

  1. let a = 123.let b=a.b=456,console.log(a)?

输出 123.

  1. innerHTML 和 innerText 的区别?

innerHTML 设置或获取元素 html 结构,再写入是会自动构建 DOM.
innerText 设置或获取元素内包含的文字内容.

  1. 创建 DOM 节点的方式?

doument.createElement()
document.write()
document.innerHTML()

  1. JS 改变 this 指向的方法.区别

bind,apply.call
区别: bind 改变 this 指向,返回一个新函数.将函数内部的 this 设置为传入的第一个参数.并不立即执行.
call.apply 会调用函数,传入上下文和其参数,并立即执行.在写法上有区别.
call 只接受两个参数,一个 this 指向的对象,另一个是数组或者类数组.
apply 除了第一个对象外,可以接收多个参数.

1
2
3
4
5
6
7
<input id='btn' type='button' value='123' />
<script>
var value = '456'
document.getElementById('btn').onclick = function(){
console.log(this.value) //打印123
}
</script>
  1. new 一个对象经历那些过程?

  2. 创建新对象. 2.将新对象的原型绑定到构造函数的原型上. 3.将构造函数的作用域赋值给新对象(因此 this 指向了这个对象). 4.执行构造函数中的代码(为新对象添加属性) 5.返回新对象.

1
2
3
4
5
6
7
8
9
10
//构造函数
function Person(name) {
this.name = name;
}
function newFunc(name) {
var o = {};
o.__proto__ = Person.prototype;
Person.call(o, name);
return o;
}

实现一个new

1
2
3
4
5
6
7
function create() {
let obj = {};
let Con = [].shift.call(arguments);
obj.__proto__ = Con.prototype;
let result = Con.apply(obj, arguments);
return result instanceof Object ? result : obj;
}
  1. map 和 for 循环的区别

map:对传入数组进行处理,返回一个新数组,原数组不做改变.
for:遍历数组的方法.

  1. Vue 中 created 和 mounted 有什么区别?请求数据在那个生命周期?

页面已经创建还未挂载到 DOM 节点上.
mounted 属于已经挂载到 DOM 上.
初始化数据一般放在 created(),如果需要改变 DOM 放在 mounted()

小明面试题

  1. 判断类型的几种方法.

instanceof
typeof
constructor
toString(obj 类型直接使用 toSting,而其他类型需要 call=>Object.prototype.toString.call(12) //[object Number]

  1. 原生 JS 怎么获取一个元素的宽度.

document.getElementById(‘div’).offsetWidth

  1. 遍历数组和对象的几种方法

map,forEach,for,for..in,for…of

  1. 权限鉴定
  2. vue 中兄弟组件传值

定义一个 bus 中介,使用 eventBus,将值传给 bus,再从 bus 中传给兄弟

  1. 实现一个方法判断入参是否为 JSON
1
2
3
typeof str == "object" &&
Object.prototype.toString.call(str).toLowerCase() == "[object Object]" &&
!str.length;
  1. 常见的伪数组有那些?怎么转化为真数组

无法调用数组的全部方法,但是仍可以用遍历.
特点: 按索引方式存储数据,具有 length 属性,没有数组的 push,shift,pop 等方法.
常见: function 的 arguments 对象,getELementsByTagName 等 NodeList 对象.
转化方法:
使用Array.prototype.slice.call()
使用Array.from()
使用[].slice.call()

  1. 如何在 node 工程中添加自定义指令和环境变量

npm script 编辑
开发环境:env_develop
生成环境:env_production

  1. 封装 http 请求以及 restful 的了解
1
2
3
4
5
6
7
8
const http = function(...options){
const { type,
return new Promise(resolve,reject){
resolve()
}catch(){
reject()
}
}

RESTful:

  1. 使用 HTTP 动词表示增删改查(CRUD)资源, GET:查询,POST:新增,PUT:更新,DELETE:删除
  2. URL 设计.URL 里使用名词而不是动词.名词最好是复数形式.减少层级,除了第一级,其他使用字符串表示.
  3. 状态码.状态码必须精确
  4. 服务器响应. 不要返回纯文本.应该返回 JSON 对象.
  5. 多个 promise 如何处理?保持顺序如何处理

Promise.all()
Promise.race()
async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//使用reduce
function run(arr, value) {
return arr.reduce((a, b) => a.then(b), Promise.resolve(value));
}
// promise function 1
function p1(a) {
return new Promise((resolve, reject) => {
resolve(a * 5);
});
}
// promise function 2
function p2(a) {
return new Promise((resolve, reject) => {
resolve(a * 2);
});
}

// function 3 - will be wrapped in a resolved promise by .then()
function f3(a) {
return a * 3;
}

// promise function 4
function p4(a) {
return new Promise((resolve, reject) => {
resolve(a * 4);
});
}

const promiseArr = [p1, p2, f3, p4];
run(promiseArr, 10).then(console.log);
  1. 函数 1 调用函数 2,如果想在函数 2 里终止函数 1,如何操作?
1
2
3
4
5
6
7
8
9
10
11
12
function a() {
b();
if (b === 3) {
return;
}
console.log("a");
}
function b() {
console.log("b");
return (b = 2);
}
a();
  1. 用闭包实现 localStorage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function configCache(){
let _cache = new Map()
return {
getCache(k){
return _cache[k]
},
setCache(k, v){
_cache[k] = v
return this
},
removeCache(k){
delete _cache[k];
return this
}
}
}();

  1. 用 setTimeout 实现 setInterval

setTimeout 只执行一次.
setinterval 是定时执行多次

1
2
3
4
5
6
7
function interval(fn, ms) {
let set = () => {
setTimeout(set, ms);
fn();
};
setTimeout(set, ms);
}
  1. 写一个函数拍平.禁止用 float

遍历传入数组,如果某一元素是数组,那就取出,放入原数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//递归
function flat(arr) {
let res = [];
let each = function (arr) {
arr.forEach((item) => {
if (item instanceof Array) {
each(item);
} else {
res.push(item);
}
});
};
each(arr);
return res;
}
let a = [1, [2, 3], [6, [5, 7]], 4];
console.log(flat(a));
//使用reduce
function flat(arr) {
return arr.reduce((a, b) => {
return Array.isArray(b) ? [...a, ...flat(b)] : [...a, b];
}, []);
}
let arr = [1, [2, 3], [6, [5, 7]], 4];
console.log(flat(arr)); // [1, 2, 3, 6, [5, 7], 4],

//转化字符串toString
function flat(arr) {
return arr
.toString()
.split(",")
.map((item) => +item);
}
let arr = [1, [2, 3], [6, [5, 7]], 4];
console.log(flat(arr));

小明机选题

五大件

state,actions,mutations,getter,modules

区别

state: 主要存放用户数据
mutations: set 用户数据(同步形式)
actions: 主要是异步操作,通过 commit  触发 mutations 中的操作改变 state
getters: change 用户数据(经常性的)
modules: 将近似的操作抽离放到一个模块里,防止 store 过于臃肿.

Vuex 中 action 的{commit}的理解

1
2
3
4
5
6
7
8
9
10
actions: {
increment (context) {
context.commit('increment')
}
}
//解构赋值
actions: {
increment({commit}){
commit('increment')
}

理解

context 是默认参数,将 {commit}  从 context 中解构出来.
let { commit } = context 
那么 commit  就是 context  对象中的一个参数,直接赋值调用即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
// options 上的属性表示附加参数
function setCookie(name, value, options) {
options = options || {};
let secure = options.secure,
path = options.path,
domain = options.domain,
expires = options.expires;
// 设置 cookie 的代码
}
//可以改写为:对options进行解构并赋予默认值
function setCookie(name, value, { secure, path, domain, expires } = {}) {
// ...
}

参数 options 默认有 { secure, path, domain, expires }  参数,
options  默认可能空,设置它的默认值为 {} .

state

state 中的数据不能在组件中赋值,必须使用 mutation 改变.

获取 state

  1. 通过属性访问
1
2
3
4
5
6
7
8
9
10
11
import store from "@/store/index.js"; //需要引入store
export default {
data() {
return {};
},
computed: {
username() {
return store.state.username;
},
},
};
  1. 通过 this.$store.state  访问.
1
2
3
4
5
computed: {
username() {
return this.$store.state.username
}
}

mapState

1
2
3
4
5
import { mapState } from 'vuex' ... computed: mapState({ username: state =>
state.name, age: state => state.age }) //mapState字符串数组 computed:
mapState(['username','age']) //当要获取组件自己的data数据时,必须使用常规函数
computed: { ...mapState({ username: function(state){ return this.firstName + ' '
+ state.username }, age: state => state.age }) }

Action

可以执行同步和异步.,通过 commit 触发 mutations 中的操作改变 state

分发 Action

  1. 通过 dispatch  分发.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<view>
<view>{{ count }}</view>
<button @click="add">click add</button>
</view>
</template>
<script>
import store from "@/store/index.js";
export default {
computed: {
count() {
return this.$store.state.count;
},
},
methods: {
add() {
store.dispatch("addCountAction");
},
},
};
</script>

mapActions

  1. 通过 mapActions  分发.

使用 mapActions  辅助函数将组件的 methods  映射为 store.dispatch  调用(需要现在根节点注入 store )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<view>
<view>{{ count }}</view>
<button @click="addCount">click add</button>
</view>
</template>
<script>
import { mapAction } from 'vuex';
export default {
computed: {
count() {
return this.$store.state.count
}
},
methods: {
...mapActions(['addCountAction'])
//将this.addCountAction映射为this.$store.dispatch('addCountAction')

//传入参数
//将this.addCountAction(amount)映射为this.$store.dispatch('addCountAction',amount)

//传入对象
...mapActions({
addCount: 'addCountActions'
//将this.addCount()映射为this.$store.dispatch('addCountAction')
})
}
}
</script>
  1. 组合 Action

store.dispatch  可以处理被触发 action  的处理函数返回的 Promise ,并且仍返回 Promise .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
store.dispatch('actionA').then(()=>{
...
})

//在另一个action中
actions: {
actionB({ commit, state}){
return dispatch('actionA').then(()=> {
commit('someMutation')
})
}
}
//使用async/await
//假设getData()和getOtherData()返回Promise
actions: {
async actionA({commit, state}){
commit('gotData', await getData())
},
async actionB({commit, state}){
await dispatch('actionA') //等待actionA完成
commit('gotOtherData', await getOtherData())
}
}

一个 store.dispatch  在不同模块中可以触发多个 action  函数.在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行.

因为 action 是包含 promise 的,所以在组件中也可以使用 then .

1
2
3
4
5
let payload = {
username: this.userInfo.username,
date: new dateFormat().toString("date"),
};
this.getLogList(payload).then(() => this.buildLog());

getters

getters 是 store 中的计算属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
list: [
{
id: 1,
name: "商品一",
status: false,
},
{
id: 2,
name: "商品二",
status: true,
},
{
id: 3,
name: "商品三",
status: false,
},
{
id: 4,
name: "商品四",
status: true,
},
{
id: 5,
name: "商品五",
status: false,
},
];

将上面数组筛选出 status 为 true 的对象.

1
2
// vuex中 getters: { activeList(state) { return state.list.filter(v=>v.status) }
}

在页面中获取的方法

1
2
3
4
<div> {{ $store.getters.activeList}} </div>

//第二种写法 computed(){ list(){ return this.$store.getters.activeList } } //
第三种 computed:{ ...mapGetters(['aciveList']) }

响应式

只有当实例被创建时就已经存在于 data 中的 property 才是响应式的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var data = { name: "yck" };
observe(data);
let name = data.name; // -> get value
data.name = "yyy"; // -> change value

function observe(obj) {
// 判断类型
if (!obj || typeof obj !== "object") {
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}

function defineReactive(obj, key, val) {
// 递归子属性
observe(val);
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 自定义函数
get: function reactiveGetter() {
console.log("get value");
return val;
},
set: function reactiveSetter(newVal) {
console.log("change value");
val = newVal;
},
});
}

响应式原理

Vue2.0

响应式主要是三个部分,Observer 进行数据劫持,Dep 进行依赖收集发生变化触发观察者,Watcher 生成观察者更新视图,将观察者的实例挂载到 Dep 类中,(Dep.target = Watcher 的实例).数据变化,调用回调更新视图.

  • 首先,利用Object.defineProperty将传入的对象属性添加 getter,setter.
  • 当数据读取时, 在 getter 中进行依赖收集(Dep),也就是把当前对象的 watcher 实例添加到依赖中,
  • 在 setter 中设置依赖通知,在数据变化的时候,依赖通知会通知所有的 watcher 实例执行更新,比如 watch 和 computed 就执行自定义的方法.

Vue3.0

Vue3 最大的变化时是 Observer 响应式中使用了 Proxy.
解决的 Vue2 中:

  1. 对属性的添加,删除的监测(之前使用$set)
  2. 对数组基于下标的修改,.length 修改的监测(数组内部变化,之前重写了数组方法)
  3. 对 Map,Set 等的支持

依赖收集

Vue3 通过track收集依赖,通过trigger触发更新.本质上通过weakMap, Map,Set实现.

模板字符串

1
`(${monthLine.label})`;

数据传递

1
2
3
4
5
6
7
v-model='value'
//等同于
:value='value'
@input='fatherM=>this.fatherM = value'
//等同于
:fatherM='fatherM'
:value.sync='fatherM=>this.fatherM = value'

给父组件传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//第一种
this.$emit('useMoney',100)
//父组件绑定useMoney事件
<Father @useMoney="useMoney" />
//父级再出发,向上传递

//第二种
this.$parent.$emit('useMoney',100)
//直接触发父级上的useMoney事件,不用再写触发事件的methods

//第三种
//封装方法
//main.js
extendEvent(Vue)
//event.js
export default function(Vue){
Vue.prototype.$eventDispatch = function(name, value){
let parent = $parent

while(parent){
parent.$emit(name, value)
parent = parent.$parent
}
}

Vue.prototype.$eventNotice = function(name,value){
let bc = children => {
children.map(c =>{
c.$emit(name, value)
if(c.$children){
bc(C.$children)
}
})
}
bc(this.$children)
}
}

监听的顺序

关于$emit$on的顺序问题,注意是先开启$on进行监听,然后触发$emit.否则触发的时候,$on并没有执行.也就监听不到.
比如使用uni.$emit进行触发时,如果跳转到另一个页面,$on是监听不到的,因为已经先触发了emit.
正确使用方法: 比如需要回退,那么先打开监听$on,然后在另一页面触发后,回退到该页面,该页面就已经监听到了.

组件传值

子组件通过$emit给父组件传值,附带参数. this.$emit('change', data) .
传递多个参数可以使用函数,this.$emit('change', {data0, data1})父组件在页面接收时不用写接收参数.@change='change()'  括号里不用写参数.函数里再写.e

nextTick

  1. created中进行 DOM 操作一定要放在Vue.nextTick
  2. 在数据变化后要执行某个操作,而这个操作需要使用随数据改变而改变 DOM 结构时,要放在Vue.nextTick()中.
  3. 操作数组中的 ref 时,注意 ref 的 VueComponent 节点渲染也是数组,要取数组的第一个,即后面要加[0].
1
this.$nextTick(() => this.$refs[`sub${index}`][0].init());

数据劫持

Vue2 中无法使用Object.defineproperty给数据添加getter/setter属性,但是这个是 Object 的方法,不能操作数组.就要对数组方法重写.即数据劫持.
要重写的是改变数组的方法,一共 7 个.push,pop,shift,unshift,sort,splice,reserve.

生命周期

created: DOM 创建但是未挂载,所以这里访问不了 DOM,一般用作调用 api.Vue3 中的 setup 语法题里也获取不到 DOM.
mounted: 页面挂载后,这里就可以拿到 DOM 了.

动态参数

v-指令后面可以跟一个动态参数,用方括号[]包裹.里面是一个 js 表达式的结果.

1
2
3
4
//bind后的动态参数表达响应式的绑定这个值
<a v-bind:[attr] = "url"></a>
//v-on后面的参数也就是个动态的事件名
<a v-on:[eventName]="doSometing"></a>

约束

动态参数预期要一个字符串,异常情况下值为null.这个null会被用于移除绑定.
空格和引号在方括号里是无效的.可以用计算属性替代这种写法.
这里不要用大写,因为会被浏览器强制转换.

动态 class 与 style

v-bind:class接收一个对象以动态切换 class.

1
2
3
4
5
6
<div v=bind:class="{active: idActice, 'text-danger': hasError }"></div>

data : {
isActive: true,
hasError: false
}

计算属性

计算属性所计算的值不用在data中声明.可以说是在computed中声明的一个参数.利用this中的参数去处理之后得到这个值.处理完要return出去.

原理

动态 class 和计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div :class="classObject"></div>

data: {
isActive: true,
error: null
}
computed: {
classObiect(){
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}

数组语法

1
2
3
4
5
6
7
8
9
<div :class="[activeClass, errorClass]"></div>

data: {
activeClass: 'active',
errorClass: 'text-danger'
}

//或者三元表达式
<div :class="[isActive ? activeClass : '', errorClass]"></div>

数组语法加对象语法

1
<div :class="[{ active: isActive }, errorClass]"></div>

绑定内联样式

对象语法

1
2
3
4
5
6
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
activeColor: 'red',
fontSize: 30
}

数组语法

1
<div :style="[activeColor, baseStyle]"></div>

v-for

v-for第一个参数是 value,第二个可以是键名name,第三个可以是索引index.

key

key 的特殊属性用于虚拟 dom 算法中对比新旧 nodes,辨识 VNodes.
如果不使用 key,Vue 会就地复用相同类型元素.可能会造成组件没有重新渲染.
使用 key,它会基于 key 的变化重新排列元素顺序,并会移除 key 不存在的元素.
下列场景适合使用 key:

  • 完整触发组件的生命周期钩子
  • 触发过渡
1
2
3
<transition>
<span :key="text">{{text}}</span>
</transition>

当 text 发生改变时, 总是会被替换而不是被修改,因此会触发过渡。

搭配计算属性

1
2
3
4
5
6
7
8
9
10
<li v-for="n in evenNumbers">{{ n }}</li>

data: {
numbers: [1,2,3,4,5]
},
computed: {
eventNumbers(){
return this.numbers.filter(number => number %2 === 0)
}
}

多维数组循环使用方法代替计算属性

1
2
3
4
5
6
7
8
9
10
11
12
<ul v-for"set in sets">
<li v-for="n in event(set)">{{ n }}</li>
</ul>

data: {
sets: [[1,2,3,4,5],[6,7,8,9,10]]
},
methods: {
even(numbers){
return numbers.filter(number => number%2 === 0)
}
}

v-for 和 v-if 搭配

不推荐二者在同一个元素使用,而且v-for优先级比v-if高.
但是如果想要部分渲染节点,不符合条件的不渲染可以用 v-if.

v-on

v-on可以传事件名,接参数,也可以访问原始 DOM 事件.

1
2
3
4
5
<button @click="greet"></button>

<button @click="alert(msg)"></button>

<button @click="warn('msg', $event)"></button>

v-model

在自定义事件上的 v-model 可以拆分为

1
<input v-bind:value="search" v-on:input="search=$event.target.value" />

在组件上的v-model可以拆分为

1
2
3
4
5
6
7
8
<custom-input
v-bind:value="search"
v-on:input="search = $event"
><custom-input>
//组件代码应为
<input v-bind:value="value"
v-on:input="$emit('input',$event.target.value)"
/>

computed VS methods VS watch

点击 button,value改变,watch 监听这个value,变化后在 watch 中导致 watchValue 变化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div>
<ul>
<li>{{computedValue}}</li>
<li>{{computedValue}}</li>
<li>{{computedValue}}</li>
</ul>
<ul>
<li>{{methodsValue}}</li>
<li>{{methodsValue}}</li>
<li>{{methodsValue}}</li>
</ul>
<ul>
<li>{{watchValue}}</li>
<li>{{watchValue}}</li>
<li>{{watchValue}}</li>
</ul>
<button @click="changeValue">change</div>
</div>
</template>
<script>
data(){
return {
value: 1,
watchValue:1
}
},
watch:{
value(val){
console.log("watch");
//刚加载并未执行,因为没有发生变化.
//再次点击change,只打印一次
//如果要加载就执行,可添加immediate,函数就变成handler
this.watchValue = val + 1
}
},
computed: {
computedValue(){
console.log('computedValue'); //打印1次,有缓存,不变化不重复计算
return this.value + 1
}
},
methods:{
methodsValue(){
console.log('methodsValue'); //打印3次
return this.value + 1
},
changeValue(){
this.value = 2
}
}
}
</script>

watch

这里 watch 的一个特点是,最初绑定的时候是不会执行的,要等到 firstName 改变时才执行监听计算。那我们想要一开始就让他最初绑定的时候就执行改怎么办呢?我们需要修改一下我们的 watch 写法,修改过后的 watch 代码如下:

1
2
3
4
5
6
7
8
9
10
11

watch: {
firstName: {
handler(newName, oldName) {
this.fullName = newName + ' ' + this.lastName;
},
// 代表在wacth里声明了firstName这个方法之后立即先去执行handler方法
immediate: true
}
}

注意到 handler 了吗,我们给 firstName 绑定了一个 handler 方法,之前我们写的 watch 方法其实默认写的就是这个 handler,Vue.js 会去处理这个逻辑,最终编译出来其实就是这个 handler。
而 immediate:true 代表如果在 wacth 里声明了 firstName 之后,就会立即先去执行里面的 handler 方法,如果为 false 就跟我们以前的效果一样,不会在绑定的时候就执行。

箭头函数不能在 watch 中写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误写法
watch: {
studentId: (newData, oldData) => {
this.disabled = !newData
console.log(this, 'this')
}
},

// 正确写法
watch: {
studentId: function (newData, oldData) {
this.disabled = !newData
console.log(this, 'this')
}
}

箭头函数会改变 this 的指向,在 Vue 组件中,最好不要随便使用箭头函数。特别是 watch 以及生命周期中。!文档也有相应的提示
image.png

computed 和 watch 区别

计算属性是根据组件数据派生出新数据.使用方式是设置一个函数,返回计算后的结果.具备缓存性质,如果依赖项不变,是不会重新计算的.
监听器 watch 可以监听某个响应式数据的变化并执行副作用.使用方式是传递一个函数,并执行副作用.一般用于请求接口.
场景上:
计算属性一般是在模板中表达式过于复杂就可以写在计算属性中.
监听器是在状态变化后做一些额外的 DOM 操作或者异步请求.

动态组件

通过componentis属性.

1
<component :is="currentTabComponent"></component>

currentTabComponent可以是已注册组件的名字或一个组件的选项对象.

is的其他用处

比如在table中,加入自定义组件会失效.

1
2
3
4
5
6
7
8
//失效
<table>
<blog-post-row></blog-post-row>
</table>
//生效
<table>
<tr is="blog-post-row"></tr>
</table>

另外,如果从以下条件使用模板,不存在限制.
字符串(例如: template:"...")
单文件组件(.vue)
<script type="text/x-template">

非 prop 的 Attribute

指传向一个组件,但是该组件并没有相应 prop 定义的 attribute.

禁用 Attribute 继承

如果不希望组件的根元素继承 attribute,可以设置inheritAttrs: false.
禁用继承和$attrs可以手动决定这些attribute会被赋予那个元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</label>
</template>
<script>
export defalut {
inheritAttrs: false,
name: 'base-input',
props:['label','value'],
data(){}
</script>

注意inheritAttrs: false选项不会影响styleclass的绑定.

这个模式允许使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素.

1
2
3
4
5
6
<base-input
label="Username:"
v-model="username"
required
placeholder="Enter your name"
></base-input>

$listeners

Vue 提供的一个属性,是个对象.包含了作用在这个组件上的所有监听器.
在一个组件的根元素上无法监听到原生事件时可以使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<label>
{{ label }}
<input v-bind="$attrs" v-bind:value="value" v-on="inputListener" />
</label>
</template>
<script>
export default {
name: 'base-input',
inheritAttrs:false,
props: ['label', 'value'],
computed: {
inputListener(){
var vm = this
return Object.assign({},this.$listeners,
{input: function(event){
vm.$emit('input', event.target.value)
}
})
}
}
</script>

.sync修饰符

.sync相当于update:myPropsName的模式.
比如更新 title

1
2
3
4
5
6
7
8
this.$emit('update:title', newTitle)

<text-docunment
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

<text-document :title.sync="doc.title"></text-document>

注意带有.sync修饰符的v-bind不能和表达式一起使用(例如v-bind:title.sync="doc.title + '!'"是无效的).取而代之的是只能使用想要绑定的 property 名,类似v-model.

多个 prop

1
<text-document v-bind:sync="doc"></text-document>

插槽

规则

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的.

v-slot 只能添加在<template>

作用域插槽

如果想在插槽中访问到子组件的数据,就得用作用域插槽.

1
2
3
4
5
6
7
8
9
10
11
12
13
//把要访问的数据绑定到slot上 //子组件
<span>
<slot v-bind:user="user">
{{user.lastName}}
</slot>
</span>

//上面的user数据绑定到了user上.被称为插槽prop //父级作用域中使用v-slot访问
<current-user>
<template v-slot:default="slotProps">
{{slotProps.user.firstName}}
</template>
</current-user>

解构插槽 Prop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//上面示例中,父级的slotProps可以解构
<current-user v-slot="{ user }">
{{user.firstName}}
</current-user>

//多个prop可以重命名
<current-user v-slot="{ user: person }">
{{person.firstName}}
</current-user>

//设定初始值
<current-user v-slot="{ user = { firstName: 'Tom' } }">
{{user.firstName}}
</current-user>

动态插槽

1
2
3
4
<base-layout>
<template v-slot:[slotName]>
</template>
</base-layout>

动态组件&异步组件

动态组件上使用 keep-alive

is 动态切换时会重新渲染,如果不需要重新渲染可以用keep-alive缓存下来.

1
2
3
<keep-alive>
<component :is="currentTabcomponent"></component>
</keep-alive>

异步组件

就是异步导入,返回 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//标准写法
Vue.component("template", function (resolve, reject) {
setTimeout(function () {
resolve({ template: "<div>I am async!</div>" });
}, 1000);
});
//require语法
Vue.component("template", function (resolve) {
require(["./my-component"], resolve);
});
//import写法
Vue.component("template", () => import("./my-component"));
//局部注册
new Vue({
components: {
"my-component": () => import("./my-component"),
},
});

加载状态

1
2
3
4
5
6
7
8
9
10
11
const AsyncComponent = () => ({
component: import("./MyComponent.vue"),
//异步组件加载时所用的组件
loading: LoadingComponent,
//加载失败使用的组件
error: ErrorComponent,
//展示加载时组件的延时时间
delay: 200,
//超时时间
timeout: 3000,
});

处理边界情况

也就是操作其他组件内部或者手动操作 DOM 的情况.

根实例

1
2
3
this.$root.foo;
//写入
this.$root.foo = 2;

父组件实例

this.$parent.xxx

子组件实例或子元素

1
2
3
4
5
6
7
8
9
10
11
12
13
<base-input ref="usernameInput"></base-input>

//this.$refs访问这个实例
this.$refs.usernameInput

<input ref="input">

//通过其父级组件定义方法:
methods: {
focus(){
this.$refs.input.focus()
}
}

$refs只会在组件渲染完成之后生效.并且不是响应式的.仅作为一个操作子组件的应急方案.所以应该避免在模板或计算属性中访问$refs.

依赖注入

如果层级过多,使用$parents不方便.就要使用依赖注入,即provideinject.

1
2
3
4
5
6
7
8
9
//父级组件中声明一个要给子组件的参数,放到provide中
provide: function(){
return {
getMap: this.getMap
}
}

//在后代组件中,使用inject接收
inject: ['getMap']

可以把依赖注入看做一部分”大范围有效的 prop”.

程序化的事件监听器

$on(eventName, eventHandler)侦听一个事件
$once(eventName, eventHandler)一次性侦听一个事件
$off(eventName, eventHandler)停止侦听一个事件
用法示例:

1
2
3
4
5
6
7
8
9
10
//声明周期结束前销毁方法
mounted: function(){
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
this.$once('hook:beforeDestroy', function(){
picker.destroy()
})
}

循环利用

递归组件

组件是可以在他们自己的模板中调用自身的.不过只能通过name来做这件事.为了避免无限循环,确保递归调用是条件性的(例如使用一个最终会得到falsev-if.

组件之间的循环引用

当首先组件 A 依赖组件 B,,然后 B 又依赖 A,如此往复.形成了循环.那么需要做一个点,A 需要 B,但不需要先解析 B.

1
2
3
4
5
6
7
8
9
10
11
12
13
//tree-folder
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents : children="folder.children"/>
</p>

//tree-folder-content
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>

处理方法

1
2
3
4
//在beforeCreate时注册它
beforeCreate(){
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者在本地注册时,使用异步导入,

1
2
3
components: {
TreeFolderContents: () => import("./tree-folder-contents.vue");
}

mixin

递归合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行”合并”.
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mixin = {
data() {
return {
message: "hello",
foo: "abc",
};
},
};
new Vue({
mixins: [mixin],
data() {
return {
message: "goodbye",
bar: "def",
};
},
created: function () {
console.log(this.$data);
//=>{ message:"goodbye', foo: 'abc', bar: 'def' }
},
});

同名钩子函数合并为一个数组,混入对象的钩子在组件自身的钩子之前调用.
如果createdmixin中有同名方法,两个生命周期都会执行,执行的方法按照页面的方法执行.
值为对象的选项,如methods,componentsdirectives,将被合并为同一个对象.两个对象键名冲突时,取组件对象的键值对.

全局混入

1
2
3
4
5
6
7
8
9
10
11
12
Vue.mixin({
created() {
var myOption = this.$options.myOption;
if (myOption) {
console.log(myOption);
}
},
});

new Vue({
myOption: "hello!",
});

自定义指令

钩子函数

一个指令定义对象可以提供以下钩子函数(均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用.在这里可以进行一次性的初始化设置.
  • inserted被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
  • update所在组件的 VNode 更新时调用.但是可能发生在其子 VNode 更新之前.指令的值可能发生了改变,也可能没有.
  • componentUpdated指令所在组件的 VNode 及其子 VNode 全部更新后调用.
  • unbind只调用一次,指令与元素解绑时调用.

钩子函数参数

  • el指令所绑定的元素,可以用来直接操作 DOM
  • binding一个对象.包含以下 property:
  1. name指令名,不包含v-前缀.
  2. value指令的绑定值.例如v-my-directive="1+1"中,绑定值为 2.
  3. oldValue指令绑定的前一个值.仅在updatecomponentUpdated钩子中可用.无论值是否改变都可用
  4. expression字符串形式的指令表达式.例如v-my-directive="1+1"中,表达式为"1+1".
  5. arg传给指令的参数.可选.例如v-my-directive:foo中参数为”foo”.
  6. modifiers一个包含修饰符的对象.例如v-my-directive.foo.bar,修饰符对象为{ foo: true, bar: true }
  7. vnodeVue 编译生成的虚拟节点.
  8. oldVnode上一个虚拟节点,仅在updatecomponentUpdated钩子可用.

    除了el之外,其他参数都应该是只读的,切勿进行修改.如果需要在钩子之间共享数据,建议通过元素的dataset来进行.

动态指令参数

用法示例
创建一个自定义指令,用来通过固定布局将元素固定在页面上.

1
2
3
4
<div id="baseexample">
<p>Scroll down the page</p>
<p v-pin="200">Stick me 200px from the top of the page</p>
</div>
1
2
3
4
5
6
7
8
9
Vue.directive("pin", {
bind: function (el, binding, vnode) {
el.style.position = "fixed";
el.style.top = binding.value + "px";
},
});
new Vue({
el: "#baseexample",
});

这会把该元素固定到距顶部 200px 位置.如果需要固定在左侧,可以使用动态参数实现.

1
2
3
4
<div id="example">
<h3>Scroll down inside this section</h3>
<p v-pin:[direction]="200">I am pinned onto</p>
</div>

对象字面量

1
2
3
4
5
<div v-demo="{ color:'white', text: 'hello!' }"></div>

Vue.directive('demo',function(el, binding){
console.log(binding.value.color) //=> 'white'
console.log(binding.value.text) //=> 'hello!'

渲染函数 & JSX

通过 render 函数去解决重复而冗长的代码.

插件

var,let,const 区别

var的作用域是方法作用域,声明之前变量是 undefined.可以重复声明.
let的作用域是块级作用域.在声明之前使用会报错,禁止重复声明.
const是常量声明方式,声明变量时必须初始化,后面不再修改该常量的值.(声明时必须赋值)
const 不是变量的值不能改动,而是变量指向的那个内存地址不能改动.

作用域

ES2015 前,ES 只有两种作用域,分别是全局作用域和函数作用域;在 ES2015 中新增了一个块级作用域.
以前块没有独立的作用域,所以在块中定义的变量,块的外面也可以访问,如:

1
2
3
4
if (true) {
var foo = "foo";
}
console.log(foo);

这对于代码是非常不利的、不安全的,有了块级作用域,可以通过新的关键字 let 去声明变量,用法跟传统一样,只是 let 声明的变量只能在声明的代码块中使用,外部无法访问的,如:

1
2
3
4
if (true) {
let foo = "foo";
}
console.log(foo); // foo is not defined

undefined 和 null 区别

undefined 是未定义的值,是变量最原始的状态
null 是人为声明为空的值.希望表示** 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。** 在内存里的表示就是,栈中的变量没有指向堆中的内存对象

闭包

我的理解: 函数内部包含存在外部作用域的变量,且调用这个函数就形成闭包.
外部调用函数内部的变量
单纯有个函数算闭包环境,被调用了,所使用的外部变量也就无法释放了,这才形成闭包.
注意闭包的函数是 return 出来的.不 return 也可以产生闭包.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function main(i) {
return function (n) {
console.log(i);
return (i += n);
};
}
var result = 10;
var cb = main(result);
console.log(cb(2));
console.log(cb(3));
console.log(result);

// 解释
function main(i) {
// 形参i存在当前作用域
return function (n) {
console.log(i);
// 相当于
i = i + n;
return i;
};
}
// cb(2)时,先打印10,注意function(n)已经是闭包了,i经过计算变为12,但是没有释放.
// cb(3)时,先打印12, i因为闭包没有释放依旧是12, 再计算后是15.
// result 一直未被修改还是10.

位于全局作用域的闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var result = [];
var a = 3;
var total = 0;

function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
};
}
}
//tip:这里也形成了闭包。total 被外层引用没有被销毁。
foo(1);
result[0](); // 3
result[1](); // 6
result[2](); // 9

疑问:下面这个算闭包吗,答:不算,把 var 换成 let 就算闭包了.
for 循环不是函数,所以 i 是全局作用域中的变量。
如果换成 let,其实内部也是闭包的机制,当 onclick 执行是循环早已执行完毕,i 早已销毁,因为闭包的机制我们才能拿到 i 对应的值.也就是说循环的 i 已经完事了,但是块级作用域的 i 还在.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
//data是数组,数组中存储的是函数,函数中的i其实早已为3,打印出来都是3
//为什么输出3 ?
//因为在执行函数的时候 i 已经完成遍历了,
//data[i]执行后寻找i, 内部没有,向上寻找,这时i 是全局变量, 并且此时的值为3
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function () {
// 暂时取名 fn
console.log(i);
}; // i是自由变量,所以这是一个闭包
})(i);
}

data[0]();
data[1]();
data[2]();
//在data[0]执行,即fn执行,此时的全局变量 i依旧是 3(但跟fn中的i无关)
//fn内部没有i,向上查找, 找到在for内,匿名函数传进来的i值0,它依旧存在在内存中。
//所以到此不会再向上去到全局作用域中查找。所以此时会打印 0

局部变量:在函数中声明,且在函数返回后不会被其他作用域所使用的对象。下面代码中的 local* 都是局部变量。(scopes 是一块堆内存)
全局变量 ,在浏览器上为 window ,在 node 里为 global。全局变量会被默认添加到函数作用域链的最底端,也就是上述函数中 [[Scopes]] 中的最后一个,可以看下上面局部变量例子中 Scopes 的最后一个。

var:全局的 var 变量其实仅仅是为 global 对象添加了一条属性。
let / const:全局的 let/const 变量不会修改 window 对象,而是将变量的声明放在了一个特殊的对象下(与 Scope 类似)。
被捕获变量就是局部变量的反面:在函数中声明,但在函数返回后仍有未执行作用域(函数或是类)使用到该变量,那么该变量就是被捕获变量。

一般如何产生闭包

  • 返回函数
  • 函数当做参数传递

函数科里化是一种闭包.

for 循环

如果是 var 声明,for 循环不是函数,所以 i 是全局作用域中的变量。

使用 let 关键字时会产生块级作用域,for 每次循环的大括号都是一个独立的块级作用域,由于后面还要用到 i ,所以这几个块级作用域都不会销毁。
elements[0].onclick = function() { alert(i); } // 在此块级作用域中,i = 0
elements[1].onclick = function() { alert(i); } // 在此块级作用域中,i = 1
elements[2].onclick = function() { alert(i); } // 在此块级作用域中,i = 2
elements[3].onclick = function() { alert(i); } // 在此块级作用域中,i = 3

1
2
3
4
5
6
7
var element = document.getElementsByTagName("li");
var length = element.length;
for (let i = 0; i < length; i++) {
element[i].onclick = function () {
alert(i);
};
}

for 循环是同步代码,先执行,事件绑定函数是异步代码.后执行.此时 for 执行完毕,i 是最后一个值.

性能

for > for-of > forEach > map > for-in

  • for 循环当然是最简单的,因为它没有任何额外的函数调用栈和上下文;
  • for...of只要具有 Iterator 接口的数据结构,都可以使用它迭代成员。它直接读取的是键值。
  • forEach,因为它其实比我们想象得要复杂一些,它实际上是 array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;
  • map() 最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。
  • for...in需要穷举对象的所有属性,包括自定义的添加的属性也能遍历到。且 for…in 的 key 是 String 类型,有转换过程,开销比较大。

如果你需要将数组按照某种规则映射为另一个数组,就应该用 map。
如果你需要进行简单的遍历,用 forEach 或者 for of。
如果你需要对迭代器进行遍历,用 for of。
如果你需要过滤出符合条件的项,用 filterr。
如果你需要先按照规则映射为新数组,再根据条件过滤,那就用一个 map 加一个 filter

for…in

是 ES5 版本发布的。以任意顺序遍历一个对象的除 Symbol 以外的可枚举属性。
迭代数组时,for...in是下标,for...of是值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 遍历对象
let profile = {name:"April",nickname:"二十七刻",country:"China"};
for(let i in profile){
let item = profile[i];
console.log(item) // 对象的键值
console.log(i) // 对象的键对应的值
// 遍历数组
let arr = ['a','b','c'];
for(let i in arr){
let item = arr[i];
console.log(item) // 数组下标所对应的元素
console.log(i) // 索引,数组下标
// 遍历字符串
let str = "abcd"
for(let i in str){
let item = str[i];
console.log(item) // 字符串下标所对应的元素
console.log(i) // 索引 字符串的下标
}

for…of

我是 ES6 版本发布的。在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 迭代数组数组
let arr = ['a','b','c'];
for(let item of arr){
console.log(item)
}
// logs 'a'
// logs 'b'
// logs 'c'
// 迭代字符串
let str = "abc";
for (let value of str) {
console.log(value);
}
// logs 'a'
// logs 'b'
// logs 'c'
// 迭代map
let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]
for (let entry of iterable) {
console.log(entry);
}
// logs ["a", 1]
// logs ["b", 2]
// logs ["c", 3]
// 迭代map获取键值
for (let [key, value] of iterable) {
console.log(key)
console.log(value);
}
// 迭代set
let iterable = new Set([1, 1, 2, 2, 3, 3,4]);
for (let value of iterable) {
console.log(value);
}
// logs 1
// logs 2
// logs 3
// logs 4
// 迭代 DOM 节点
let articleParagraphs = document.querySelectorAll('.article > p');
for (let paragraph of articleParagraphs) {
paragraph.classList.add("paragraph");
// 给class名为“article”节点下的 p 标签添加一个名为“paragraph” class属性。
}
// 迭代arguments类数组对象
(function() {
for (let argument of arguments) {
console.log(argument);
}
})(1, 2, 3);
// logs:
// 1
// 2
// 3
// 迭代类型数组
let typeArr = new Uint8Array([0x00, 0xff]);
for (let value of typeArr) {
console.log(value);
}
// logs:
// 0
// 255

函数柯里化

高阶函数: 函数可以作为参数传递 && 函数可以作为返回值输出
柯里化(Currying): 把接受多个参数的函数变换成接受一个单一参数(或部分)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
示例:
编写一个 add 函数,使得 add(1,2)和 add(1)(2)都可以执行,并返回 3.

1
2
3
4
//需要判断参数值的长度
add(a,b){
if(arguments.length === 1 ? b => a + b : a+b
}

string 和 String 的区别

String 是包装类,是一个特殊的 object.
也就是说当不用 new 的时候,String(…) === toString(…)

new String() 和 String()区别

我们知道 new 关键字的过程涉及到新对象的创建,所以,new String(str)的结果返回的一个新的 String 实例,所以,b 和 b2 保存的是两个对象的引用,他们的引用地址不一样,直接比较的话,逻辑引用类型的比较是一样的,结果就是不相等。

switch 语句

注意: 如果没写 break,下面的语句也会执行,不管是否符合条件。
default 是条件都不满足才执行。

1
2
3
4
5
6
7
8
9
10
11
switch (expression) {
case 条件1:
console.log("满足条件1");
// 如果这里没写break,下面条件2的语句也会执行,不管是否符合条件2
case 条件2:
console.log("满足条件2");
break;
default:
console.log("和上两个条件都不相等");
break;
}

expression  中是可能变化的量,变化的可能性有三种,条件 1,2,和不满足条件 12 的其他种类.

空数组 push 后,再次调用记得赋值为空.

map 和 forEach

map 返回处理后的新数组,而不是原数组的处理.原数组是不变的.

能用 forEach()做到的,map()同样可以。反过来也是如此。
map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
forEach()允许 callback 更改原始数组的元素。map()返回新的数组。

区别

forEach 执行后返回 undefined
map 执行后返回新数组

共同点

只能遍历数组并参数都一样
不改变原函数(引用类型除外)
无法中断循环;return 只是结束本次循环,进入下一次循环
break 或 continue 都将会报错

forEach 和 map 可以通过return false跳出本次循环,但是不能中断整个循环。
break 和 continue 也会报错。想要中断,可以使用thorow new Error
for/for…of: break 跳出本次循环;continue 结束本次循环执行下一次循环,没有 return。
for…in:会忽略 break || continue。没有 return。

1
2
3
4
5
6
7
8
9
10
11
12
13
let list = [
{ id: "1", time: "10:00.000" },
{ id: "2", time: "11:00.000" },
{ id: "3", time: "12:00.000" },
];
let listMap = list.map((item) => item.time.split(".").splice(0, 1).join(""));
//listMap返回时被处理的时间的字符串数组,原数组list没有变化
console.log(listMap); //["10:00", "11:00", "12:00"]
let listMap2 = list.forEach((item) =>
item.time.split(".").splice(0, 1).join("")
);
//forEach不返回新数组
console.log(listMap2); //undefined

数组返回

注意: map,filter,find,findIndex等必须要return才能生效.

类数组转数组方法

  1. Array.prototype.slice.call(obj) // 因为 slice 返回一个新数组,原数组不变。
  2. Array.from
  3. 扩展运算符

Array.from

该方法对一个类数组或可迭代对象创建一个浅拷贝的数组实例。
方法接收 3 个参数,
第一个是类数组或可迭代对象,
第二个参数是个回调,新数组的每个元素都会执行该回调,
第三个参数是执行第二个回调函数的 this 对象。

1
2
Array.from([1, 2, 3], (x) => x * 2); // [2,4,6]
Array.from({ length: 5 }, (v, i) => i); // [1,2,3,4,5]

reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let people = [
{ name: "Alice", age: 21 },
{ name: "Max", age: 20 },
{ name: "Jane", age: 20 },
];

function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
let key = obj[property];

if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
// console.log(acc)
//这里acc[key]是value,也就是value里push
return acc;
}, {});
}

let groupedPeople = groupBy(people, "age");
// groupedPeople is:
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }

reduce 的理解

reduce 接收由数组调用,接收一个函数,和一个可选参数,初始值。如果没有初始值,默认初始值是函数的第一个参数。函数有 3 个参数,分别是累加器 acc,当前值 cur,当前值的索引 index。

reduce 有几种常用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 累加
const arr = [1, 2, 3];

arr.reduce((acc, cur) => {
// return 就是下一次累加的结果
return acc + cur;
}, 0);
// 流程就是第一次0+1
// 第二次 1 + 2
// 第三次 3 + 3
// 最终结果6

// 多维数组转化一维数组(数组扁平化)
const arr2 = [
[1, 2],
[3, [4, 5]],
]; // => [1,2,3,4,5]

const flat = (arr) => {
arr.reduce((prev, cur) => {
// 判断cur是不是数组,如果不是数组,就把它拼接到空数组里
// concat是可以连接数组或值的
// 进入递归的数组,最后返回的也是一个一维数组
return prev.concat(Array.isArray(cur) ? flat(cur) : cur);
}, []);
};

// 统计数组中值的个数
const arr3 = ["a", "b", "c", "a", "c"]; // => {a: 2, b: 1, c: 2}
// 上面说明最终要返回一个对象

arr3.reduce((prev, cur) => {
// 如果对象中没有这个值,那就把它放进去,赋值一次
if (!prev[cur]) {
prev[cur] = 1;
} else {
// 如果已经出现了,次数+1
prev[cur]++;
}
// 把这个对象返回出去
return prev;
}, {});

// 实现Promise按顺序执行
const arr = [p1, p2, p3];
arr.reduce((prev, cur) => {
// 就是把链式调用使用reduce循环了
return prev.then(cur);
}, Promise.resolve());

// 实现map
if (!Array.prototype.mapUseReduce) {
Array.prototype.mapUseReduce = function (callback, thisArgs) {
// this就是调用的数组
return this.reduce(function (prev, cur, index, array) {
// map中回调需要的参数正好是reduce的后三个参数
// map执行就是循环每一项,执行回调
prev[index] = callback.call(thisArgs, cur, index, array);
return prev;
}, []);
};
}

// 实现组合函数
const compose =
(...fns) =>
(value) =>
fns.reverse().reduce((prev, acc) => acc(prev), value);

对象类型的指针问题

js 中,参数传递只有一种规则:按值传递,基于值的复制。原始类型复制的是值本身,所以这两份数据互不影响;引用类型复制的是引用值,所以形参和实参指向同一个对象,通过一个饮用修改了对象,那么通过另外一个引用访问的对象就是修改后的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test(person) {
person.age = 26;
person = {
name: "yyy",
age: 30,
};

return person;
}
const p1 = {
name: "yck",
age: 25,
};
const p2 = test(p1);
console.log(p1); // -> ?
console.log(p2); // -> ?

解释:
p1 =>指向对象{name:'yck',age:25}的指针00001.
test(p1)传指针,person.age = 26修改该对象.此时p1 = {name: 'yck', age:26}
接下来要给person重新赋值,但是person指向p1的指针被覆盖,重新开辟一块内存,新的指针00002.
person拥有一个新的地址.person = {name: 'yyy', age: 30}.返回出来.

运算符

逻辑非(!)

逻辑非的规则:
操作数能被转化为true的,都返回false.否则返回true.
举例:
下面都被转换成false:
null,NaN,0, '',undefined.
所以遇到!会取反转为true.

遇到对象会返回false.

1
2
3
4
let a = "0";
!a == false; // 字符串取反,就是false, 所以判断结果为true
a == false; // 使用==转化false为0, "0"和0比较,"0"被转化为0,结果为true
a == 0; //跟上一条一样

双逻辑非(!!)

用于始终返回true或者false
比如!!undefined返回false,否则直接会返回undefined.

空值合并运算符(??)

当左侧为nullundefined,返回右侧。
||的区别,当左侧是''或者0,是可以取左侧的值的。

(,)逗号运算符

逗号表达式的值是成员表达式最右侧的值。

1
2
3
4
5
6
7
8
9
let a, b, c;

(a = b = 3), (c = 4); // 值 4 返回到控制台
console.log(a); // 3 (left-most)

let x, y, z;

x = ((y = 5), (z = 6)); // 值 6 返回到控制台
console.log(x); // 6 (right-most)

二进制运算符

<<:左偏移,将数值转换为二进制,然后向左偏移多少位.
|:按位或.按照二进制转换后,位数有则计算.

1
2
3
4
5
6
7
let a = 1 << 1;
//二进制: 0000 0001 => 左偏移1位 => 0000 0010 => 2
let b = 2 << 2;
//二进制: 0000 0010 => 左偏移2位 => 0000 1000 => 8
let c = a | b;
//二进制: 0000 0010
//二进制: 0000 1000 => 0000 1010 =>10

in运算符

指定的属性在指定的对象或其原型链中,则in运算符返回 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // 返回true
3 in trees; // 返回true
6 in trees; // 返回false
"bay" in trees; // 返回false (必须使用索引号,而不是数组元素的值)

"length" in trees; // 返回true (length是一个数组属性)

Symbol.iterator in trees; // 返回true (数组可迭代,只在ES2015+上有效)

// 内置对象
"PI" in Math; // 返回true

// 自定义对象
var mycar = { make: "Honda", model: "Accord", year: 1998 };
"make" in mycar; // 返回true
"model" in mycar; // 返回true

如果只是将一个属性值赋值为undefined,而没有删除,则in运算符仍然会返回 true.

判断对象上是否有某个属性

方法有: for…in,Object.keys,in 操作符, hasownProperty。
for…in:可以遍历包括原型链上的属性
Object.keys: 遍历所有可枚举属性,不包括原型链
in 操作符: 可以检查原型链
hasOwnProperty:无法检查原型链.(返回布尔值,判断对象中是否有该值)

类型判断

值类型用**typeof**,引用类型用**instanceof**

typeof

运算符返回一个字符串,表示操作数的类型。
原始类型: 除了 null 判断为 object,其他都正常.因为 null 是全零,被判断为 object.
引用类型: 除了函数显示function,其他都是object.

instanceof

判断对象类型就可以用 instanceof,它会通过原型链判断.
但是原始类型就不行了.

判断是否是数组

  1. Array.isArray()
  2. Object.prototype.toString.call(arr)
  3. instanceof

类型转换

类型 boolean Number String
Boolean true true 1 “true”
Boolean false false 0 “false”
Number 123 true 123 “123”
Number Infinity true Infinity “Infinity”
Number 0 false 0 “0”
Number NaN false NaN “NaN”
String “” false 0 “”
String “123” true 123 “123”
String “123abc” true NaN “123abc”
String “abc” true NaN “abc”
Null null false 0 “null”
Undefined undefined false NaN “undefined”
Function function(){} true NaN “function(){}”
Object {} true NaN “[object Object]”
Array [] true 0 “”
Array [“abc”] true NaN “abc”
Array [“123”] true 123 “123”
Array [“123”,”a”] true NaN “123,a”

加法(特殊)

四则运算符合两个规则:

  1. 运算符一方为字符串,会把另一方转化为字符串.
  2. 如果一方不是字符串或数字,就会把它转化为字符串或数字.
1
2
3
1 + "1"; // "11"
true + true; // 2
4 + [1, 2, 3]; // "41,2,3"
  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 ‘11’
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 ‘a’ + + ‘b’:

`a++b`返回`aNaN`.因为`+'b'`返回`NaN`.
`+ '1'`快速获取number类型.

除了加法

只要其中一方是数字,那么另一方就会被转为数字

1
2
3
4 * "3"; // 12
4 * []; // 0
4 * [1, 2]; // NaN

==运算符

  1. 如果一个操作数是布尔值,在比较之前要转成 number.
  2. 如果一个操作数是字符串,另一个是 number,要把字符串转成 number

解释[] == ![]为 true.

1
2
3
4
// 首先转化![] 为false
// 布尔值要先转成number => false->0
// [] 就要转成number -> 0
// 0 == 0 //true

解释{} == !{}为 false

1
2
3
// 基本和上面相同
// 差异在于{} 转为number是NaN
// NaN == 0 就是false

基本类型

undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。
{ }是一个不完全空对象,原型链上有Objectnull为原型链顶端,因此Object.prototype.__proto__ === null为 true。

null 是完全空对象,原型链也没有。

相等性

严格相等:===
非严格:==
Object.is:判断是否是同一个值

0.1+0.2 != 0.3

因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。
0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。

那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002.

那么可能你又会有一个疑问,既然 0.1 不是 0.1,那为什么 console.log(0.1) 却是正确的呢?

因为在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程中发生了取近似值的过程,所以打印出来的其实是一个近似值.

如何解决

1
parseFloat((0.1 + 0.2).toFixed(10));

this 的指向

this 的指向与函数声明无关,取决于函数调用.

  1. 普通函数调用(fn()) -> this 指向 window
  2. 对象方法调用(xx.fn()) -> this 指向对象
  3. 构造函数调用(new fn()) -> this 指向 new 创建的实例对象
  4. 匿名函数中的 this:匿名函数的执行具有全局性,则匿名函数中的 this 指向是 window,而不是调用该匿名函数的对象

例如:
box.onclick = function(){}这就属于对象方法调用,this 也就指向 box.

函数调用的优先级

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

箭头函数的 this

  1. 箭头函数中的 this 是在函数定义的时候就确定下来的,而不是在函数调用的时候确定的;
  2. 箭头函数中的 this 指向父级作用域的执行上下文;(技巧:因为 javascript 中除了全局作用域,其他作用域都是由函数创建出来的,所以如果想确定 this 的指向,则找到离箭头函数最近的 function,与该 function 平级的执行上下文中的 this 即是箭头函数中的 this
  3. 箭头函数无法使用 apply、call 和 bind 方法改变 this 指向,因为其 this 值在函数定义的时候就被确定下来
1
2
3
4
5
6
7
8
9
let obj = {
//此处的this即是箭头函数中的this
getThis: function () {
return () => {
console.log(this);
};
},
};
obj.getThis()(); //obj
1
2
3
4
5
6
7
8
9
//代码中有两个箭头函数,由于找不到对应的function,所以this会指向window对象。
let obj = {
getThis: () => {
return () => {
console.log(this);
};
},
};
obj.getThis()(); //window

call,apply,bind

笨方法记不同:call=>list, apply=>arr.
不同点:

  1. 传参不同,call->多个参数,apply->数组,bind->就一个 this
  2. 执行机制不同: call,apply 立即执行,bind 不执行.

call的使用场景:
检测数据类型:

  1. typeof,缺点:无法检测null,array
  2. 使用Object.prototype.toString.call().该方法会返回固定类型格式[object type],其中 type 是类型
1
2
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call([1, 2, 3]); // [object Array]

实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(uname, age) {
this.uname = uname;
this.age = age;
}
Person.prototype.getValue = function(){
console.log(this.uname)
}
function Son(uname, age) {
Person.call(this, uname, age);
}
Son.prototype = Object.create(Parent.prototype, {
constructor() {
value: Son, // 设置constructor的值是Son,说明原型对象的构造函数是Son
enumerable: false,
writable: true,
configurable: true
}
})
var son = new Son("zhang", 12);
console.log(son);

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数
apply使用场景:
类数组转真数组:

ES5:上下文

1
2
let newArr = [];
newArr.push.apply(newArr, obj);

ES6: Array.from()
该方法可以将类数组直接转化为真数组.
取数组最大值:
ES5:

1
2
let arr = [1, 2, 3];
Math.max.apply(Math, arr);

ES6:Math.max(...arr)

bind不会立即调用函数,而是得到一个修改this后的新函数.(一次修改,终身受用)
细节: 如果在 bind 后传递参数,参数也会绑定,但是调用函数所传参数就会失效了.
修改不需要立即执行的函数:
比如事件处理函数,定时器

如果对一个函数进行多次 bind,那么上下文会是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
let a = {};
let fn = function () {
console.log(this);
};
fn.bind().bind(a)();

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function () {
return fn.apply();
}.apply(a);
};
fn2();

无论bind多少次,this 永远指向第一次 bind.

  • 对于 new 的方式来说,this 被永远绑定在了实例上面,不会被任何方式改变 this
  • 箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

    首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo()这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

原型和原型链

每个实例对象都有一个私有属性__proto__,指向构造函数的原型对象prototype
该原型对象也有自己的原型对象,层层向上直到一个对象的原型对象为 null。这构成了原型链。
null 没有原型,是原型链的最后一环。

继承

当访问一个对象的属性时,不只从该对象上寻找,还从原型链上查找,直到找到或者到达原型链的末端。

constructor 和原型对象,构造函数,实例对象

prototype属于构造函数,指向原型对象. 作用:解决内存浪费和变量污染.
constructor属于原型对象,指向构造函数.作用:可以让实例对象知道自己是被谁创建的.
__proto__属于实例对象,指向原型对象.作用:实例对象访问原型对象的成员.
image.png

如何方便理解三者关系,构造函数是父级,父级的 prototype(老婆)就是原型对象,原型对象的 constructor(老公)就是构造函数,子级实例对象的proto(母亲)就是原型对象.

实例对象是无条件继承原型对象.
原型链的作用就是继承.也就说,js 是利用原型链实现面向对象继承的.

1
2
3
4
5
6
7
8
9
10
11
//构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 原型对象
Person.prototype.eat = function () {
console.log("吃东西");
};
// 实例对象
const p1 = new Person("张三", 30);

构造函数中的方法会导致内存的浪费.
两个实例对象虽然都可以调用 eat()方法,但是两个方法堆地址不同.每次调用函数都会在堆内存中生成一块新的空间.
image.png
解决方法 1: 使用全局变量.即将函数在全局书写,在构造函数内赋值,也即只获取地址.
缺点: 导致变量污染.
解决方法 2: 使用对象将全局变量函数包裹,但是治标不治本.
最终解决方法: 原型对象.

原型对象

原型对象是构造函数的默认属性 prototype 所指向的一个空对象.用于解决内存浪费和变量污染的问题.

1
Person.prototype.eat = function () {};

与 ES6 中继承的关系

ES6 中继承的方法属于 ES5 的语法糖.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent {
constructor(value) {
this.val = value;
}
getValue() {
console.log(this.val);
}
}
class Child extends Parent {
constructor(value) {
super(value);
this.val = value;
}
}
let child = new Child(1);
child.getValue(); // 1
child instanceof Parent; // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。

instanceof

用法:检测构造函数的原型在不在实例对象的原型链上.
arr 的原型链: arr ->Array.prototype -> Object.prototype -> null

1
2
3
arr instanceof Array; //true
arr instanceof Object; //true
arr instanceof null; //typeError,右侧不是一个object

工厂函数和构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createPerson(name, age) {
let p = {};
p.name = name;
p.age = age;
return p;
}

// 构造函数
function CreatePerson(name, age) {
this.name = name;
this.age = age;
}
// 通过new关键字调用的是构造函数
let p1 = new CreatePerson("张三", 30);

new 的用途

  1. 创建空对象.方法很多种,对象字面量const obj = {},或者Object.create()
  2. 将空对象的原型指向构造函数的原型. obj.__proto__ = constructor.prototype
  3. 将空对象作为构造函数的上下文(改变 this 指向).const res = constructor.apply(obj, args)
  4. 对构造函数的返回值进行处理判断return res instanceof Object ? res : obj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 模仿new关键词实现
* @param {Function} constructor 构造函数
* @param {...any} argument 任意参数
*/
const _new = (constructor, ...argument) => {
const obj = {}; //创建一个空的简单对象
obj.__proto__ = constructor.prototype; //设置原型
//新创建的对象作为this的上下文传递给构造函数
const res = constructor.apply(obj, argument);
//如果该函数没有返回对象,则返回this(这个this指constructor执行时内部的this))。
return typeof res === "object" ? res : obj;
};

function Person(name, sex) {
this.name = name;
this.sex = sex;
}

const people = new Person("Ben", "man");
const peopleOther = _new(Person, "Alice", "woman");
console.info("people", people); // people Person { name: 'Ben', sex: 'man' }
console.info("peopleOther", peopleOther); // peopleOther Person { name: 'Alice', sex: 'woman' }
console.info("people.__proto__", people.__proto__); //people.__proto__ Person {}
console.info("peopleOther.__proto__", peopleOther.__proto__); //peopleOther.__proto__ Person {}

new 的细节

  1. 构造函数的首字母大写,为了提醒别人不要忘记 new 关键字
  2. 关于构造函数主动写 return.对于值类型,无效,还是返回 new 创建的对象,对于引用类型,会覆盖.
  3. new 的 this 问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
function User() {
this.name = 'John';

setTimeout(function greet() {
console.log(`Hello, my name is ${this.name}`); // Hello, my name is
console.log(this); // window
}, 1000);
setTimeout(()=>{
console.log(`name is ${this.name});
},1000)
}

const user = new User();

对象字面量,new Object(),Object.create()区别

字面量和 new 关键字创建的对象是 Object 的实例,原型指向 Object.prototype,继承内置对象 Object

Object.create(arg, pro)创建的对象的原型取决于 arg,arg 为 null,新对象是空对象,没有原型,不继承任何对象;arg 为指定对象,新对象的原型指向指定对象,继承指定对象

1
2
3
4
5
6
7
8
9
10
11
var company = {
address: "beijing",
};
var yideng = Object.create(company);
delete yideng.address;
console.log(yideng.address);
// 写出执行结果,并解释原因
//beijing
//这里的 yideng 通过 prototype 继承了 company的 address。
//yideng自己并没有address属性。所以delete操作符的作用是无效的。
//yideng.address只是通过原型链去继承company的address

Object.assign()

语法: Object.assign(target, ...source)
解释: 将所有可枚举属性的值从一个或多个对象分配到目标对象.最后返回目标对象.

1
2
3
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }

Object.assign()只能用于浅拷贝,无法用于深拷贝.

1
let obj = { a: 1, b: { c: 0 } };

对于这种就无法拷贝到对象 c.只能使用JSON.parse(JSON.string(obj)).
继承属性和不可枚举属性是不能被拷贝的.
原始类型会被包装,只有字符串的包装对象才可能有可枚举属性.

浅拷贝

可以使用扩展运算符实现浅拷贝

1
2
3
4
let obj = { a: 1 };
let obj2 = { ...obj };
obj.a = 2;
console.log(obj2.a); //1

深拷贝

通常可以通过 JSON.parse(JSON.stringify(object))来解决。
缺点:
忽略undefined,Symbol,函数,循环引用对象.

Event loop

执行 JS 代码就是往执行栈中放函数.遇到异步代码就挂起,需要执行的时候放到 TASK 中(有多种 task).
执行栈空了之后,从 task 中拿出要执行的代码放到执行栈中执行.

所以本质上说,异步还是同步行为.

浏览器的 event loop

  • js 引擎首先从宏任务中取出第一个(一般是 script),
  • 执行完毕后,将微任务中所有的任务取出,按顺序执行
  • 如果在这一步产生新的微任务也要执行
  • 执行完后,会进行 DOM 渲染
  • 进入下一轮 event loop,再取出一个宏任务,,执行完后取出所有的微任务,执行.
    一次eventloop循环会处理一个宏任务和所有这次循环产生的微任务

异步和同步

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

进程和线程

本质上两个名词都是 CPU 工作时间片的描述.

  • 进程是 CPU 在运行指令,加载和保存上下文所需的时间.放在应用上就代表了一个程序.
  • 线程是进程中更小的单位,描述执行一段指令所需的时间.

执行栈

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题

执行顺序

  • 先执行同步代码,属于宏任务.
  • 执行完后,执行栈空,查询是否有异步代码
  • 执行微任务
  • 执行微任务后,如有必要,会重新渲染页面
  • 开始下一轮 Event loop,执行宏任务中的异步代码,

TASK(微任务和宏任务)

在 ES6 中,微任务叫jobs,宏任务叫task.
微任务包括: process.nextTick(), Promise,MutationObserver.
宏任务包括: script,setTimeout, setInterval,setImmediate,I/O,UI rendering,DOM 事件,DOM 渲染,AJAX 请求.
而且微任务并非快于宏任务,因为宏任务包括script,浏览器会先执行一个宏任务,接下来有异步代码才会执行微任务.

微任务和宏任务之间隔了一个 DOM 渲染.
执行顺序:
微任务 > DOM 渲染 > 宏任务

关于 promise

promise 内部是同步代码,then()才是异步代码,遇到 then 就放到微任务队列中.
疑惑:
多个 then,嵌套 then 的顺序?
先将第一个 then 加入到微任务队列中,后面的拿不到状态,因为前面还是 pending,所以还无法放入队列.
等到前一个执行完毕,才加入队列.
then 中如果又 return 一个 promise,则前一个 promise 的其他 then 就挂到新的 promise 下.如果新的 promise 没有 return,那就要看有没有resolve(),有resolve()就继续走一个 then,因为resolve表示上一个 promise 状态 fulfilled 了,可以继续.而 then 中或者 await 中的 promise 就看有没有 return 了,如果是return Promise那么就直接跳出这个包裹函数了,包裹函数后面的异步就放在最后才执行.
setTimeout 中含有 promise 的顺序?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
console.log("script start");

async function async1() {
await async2(); //2.回到这里,遇到await,让出线程,执行之后的代码,所以下面的也不放到微任务队列
console.log("async1 end"); //3. 当同步代码执行完毕,回到这里,
//将resolve放到微任务队列中,执行后面then中的回调.在两次tick后执行这里的微任务.
}
async function async2() {
console.log("async2 end"); //1.立即执行,执行完返回一个Promise
await Promise.resolve(console.log("res"));
}
async1();

setTimeout(function () {
console.log("setTimeout");
}, 0);

new Promise((resolve) => {
console.log("Promise");
resolve();
})
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});

console.log("script end");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// => 代码一执行就开始执行了一个宏任务-宏0
console.log("script start");

setTimeout(() => {
// 宏 1
console.log("setTimeout");
}, 1 * 2000);

Promise.resolve()
.then(function () {
// 微1-1
console.log("promise1");
})
.then(function () {
// 微1-4 =>
// 这个then中的会等待上一个then执行完成之后得到其状态才会向Queue注册状态对应的回调,
//假设上一个then中主动抛错且没有捕获,那就注册的是这个then中的第二个回调了。
console.log("promise2");
});

async function foo() {
await bar(); // => await(promise的语法糖),会异步等待获取其返回值
// => 后面的代码可以理解为放到异步队列微任务中。 这里可以保留疑问后面会详细说
console.log("async1 end"); // 微1-2
}
foo();

function bar() {
console.log("async2 end");
}

async function errorFunc() {
try {
await Promise.reject("error!!!");
} catch (e) {
// => 从这后面开始所有的代码可以理解为放到异步队列微任务中
console.log(e); // 微1-3
}
console.log("async1");
return Promise.resolve("async1 success");
}
errorFunc().then((res) => console.log(res)); // 微1-5

console.log("script end");

// run:
// 执行微1-1: promise1
// 执行微1-2: async1 end
// 执行微1-3: error!!!、async1 。
// 当前异步回调执行完毕才Promise.resolve('async1 success'),
// 然后注册then()中的成功的回调-微1-5
// 执行微1-4: promise2
// 执行刚刚注册的微1-5: async1 success

如果 Promise 中有 setTimeout,那就看 resolve 在不在 setTimeout 里了,如果在,因为后面 then 拿不到 value,无法继续,就得等着,如果不在,那就走 then,回来再走 setTimeout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
new Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(() => { // 微1-1
console.log(2)
new Promise((resolve, reject) => {
console.log(3)
setTimeout(() => { // 宏2
reject();
}, 5 * 1000);
resolve() // TODO 注1
})
.then(() => { // 微1-2 TODO 注2
console.log(4)
new Promise((resolve, reject) => {
console.log(5)
resolve();
})
.then(() => { // 微1-4
console.log(7)
})
.then(() => { // 微1-6
console.log(9)
})
})
.then(() => { // 微1-5 TODO 注3
console.log(8)
})
})
.then(() => { // 微1-3
console.log(6)
})
// 注意then后面想执行需要前面的状态是fufilled,否则并不会放入队列
//主要是关于resolve()后进队列的问题

// 1-1进队列,
// 1-1内部的promise 是同步代码,
// 1-2进队列,同时1-1的返回值是resolve(undivided)
// 当有返回值时,1-3进队列
// 清空微任务,1-2执行,1-4进队列,同时1-2的resolve(undivided),1-5进队列,
// 执行微任务,1-3执行,1-4执行,1-6进队列,1-5执行.
// 最后1-6进执行

// 我觉得大概就是这样,then执行一定要记住then在resolve之后才会把then回调放到微任务队列去
```</div>warning
前面说过promise.finally()也是微任务,finally可以理解为不管promise的状态是成功或失败都要执行我。但是我不接受任何结果。因此finally接受不到返回值res为undefined</div>

## 任务队列中async/await的运行机制

- async定义的是一个Promise函数和普通函数一样只要不调用就不会进入事件队列。
- async内部如果没有主动return Promise,那么async会把函数的返回值用Promise包装。
- await关键字必须出现在async函数中,await后面不是必须要跟一个异步操作,也可以是一个普通表达式。
- 遇到await关键字,await右边的语句会被立即执行然后await下面的代码进入等待状态,等待await得到结果。await后面如果不是 promise 对象, await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果。await后面如果是 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
- await后面的代码放不放到最后主要看,await 的代码是不是Promise,如果是,先不放到微任务队中,也就是等同步代码执行完,再放,也就到了微任务队列的最后了,如果不是,可以放到微任务队列中.
- `await bar()` => `Promise.resolve(bar())`, 后面的代码相当于`Promise.then(...)`
- `await new Promise`如果promise没有返回值,是不执行后面内容.</div>warning
await的真实意思是 async wait(异步等待的意思)await表达式相当于调用后面返回promise的then方法,异步(等待)获取其返回值。即 await<==>promise.then</div>
<div style="background: #E8F7FF;padding:10px;border: 1px solid #ABD2DA;border-radius:5px;margin-bottom:5px;">try/catch捕获不到异步的异常,只能捕获同步代码.</div>

# 定时器

## setTimeout
setTimeout并不一定准确.
其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,我们可以通过代码去修正 setTimeout,从而使定时器相对准确.
```javascript
let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval

function loop() {
count++
// 代码执行所消耗的时间
let offset = new Date().getTime() - (startTime + count * interval);
let diff = end - new Date().getTime()
let h = Math.floor(diff / (60 * 1000 * 60))
let hdiff = diff % (60 * 1000 * 60)
let m = Math.floor(hdiff / (60 * 1000))
let mdiff = hdiff % (60 * 1000)
let s = mdiff / (1000)
let sCeil = Math.ceil(s)
let sFloor = Math.floor(s)
// 得到下一次循环所消耗的时间
currentInterval = interval - offset
console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval)

setTimeout(loop, currentInterval)
}

setTimeout(loop, currentInterval)

setInterval

接下来我们来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。
第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。
第二,它存在执行累积的问题.

requestAnimationFrame

通过 requestAnimationFrame 来实现 setInterval.
首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function setInterval(callback, interval) {
let timer;
const now = Date.now;
let startTime = now();
let endTime = startTime;
const loop = () => {
timer = window.requestAnimationFrame(loop);
endTime = now();
if (endTime - startTime >= interval) {
startTime = endTime = now();
callback(timer);
}
};
timer = window.requestAnimationFrame(loop);
return timer;
}

let a = 0;
setInterval((timer) => {
console.log(1);
a++;
if (a === 3) cancelAnimationFrame(timer);
}, 1000);

垃圾回收机制

新生代算法

存活时间短,采用 Scavenge GC 算法.
分为两部分空间,From 和 To,总是有一个空闲.
新分配的对象 => From 空间,放满为止. => 启动 GC,检查 From 空间,
存活的,复制到 To 空间, 失活的,清除. => From 和 To 空间互换.

老生代算法

  • 经历过一次 GC 的对象进入老生代空间
  • To 空间的对象占比大小超过 25% =>老生代空间

老生代采用标记-清除, 标记-紧缩算法.

JS 异步

我们都知道 JavaScript 是单线程的,如果 JS 都是同步代码执行意味着什么呢?这样可能会造成阻塞,如果当前我们有一段代码需要执行时,如果使用同步的方式,那么就会阻塞后面的代码执行;而如果使用异步则不会阻塞,我们不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。因此在 JS 编程中,会大量使用异步来进行编程

同步

所谓的同步就是在执行某段代码时,在该代码没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值之后,就可以执行其他代码了。换句话说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行,这样的情况称为同步。

异步

所谓异步就是当某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果。而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会影响阻塞后面的代码执行,这样的情形称为异步

方案

  • 回调函数
  • Promise
  • Generator
  • async/await

还有事件监听,发布订阅模式是异步操作.

react 页面是由各个 react 组件构成的.

基础

传值

各个组件之间传值,向外部传值用 this.props.... 
向内部传值用 this.state... 
在内部保存值用 this.setState

类有三个要素.
声明的参数
constructor 构造函数
调用的函数.

React 类

一般调用的是 render()  函数,将内部的 JSX  语法构建到虚拟 DOM 中.从而渲染到页面上.
在定义子类的构造函数时,都要调用 super()  方法.
因此,所有含有构造函数的 React 组件,必须以 super(props)  开头.

组件通信

父传子

子组件通过一个绑定的参数接受来自父组件的 props.父组件把值写在 state 中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Name extends React.Component {
state = {
lastName = "Tom"
}
render(){
<div className="parent">
<Child name={this.state.lastName} />
//name={this.state.lastName}作为一个整体传递给子组件,作为props
</div>
}
}

function Child(props) {
return <div>父组件的数据: {props.name}</div>
}

子传父

通过回调函数,父组件提供回调函数,子组件调用,将要传递的数据通过参数传递.
父组件拿到数据进行更新 state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Parent extends React.Component {
state = {
parentName: ''
}
//声明回调
const getChildMsg = (data) => {
console.log("来自子组件",data)
this.setState({
parentName: data
})
}

render() {
return (
<div>
<p>父组件接受: {this.state.parentName}</p>
<Child getMsg={this.getChildMsg} />
</div>
)
}
}
//子组件
class Child extends React.Component {
state = {
msg: '子组件的信息'
}

const handleClick = () => {
//触发父组件的回调,并将参数传递
this.props.getMsg(this.state.msg)
}

render() {
return (
<div className="child">
<button onClick={this.handleCick}>Click me!</button>
</div>
)
}

父级的方法传递到子级的底层时,进行调用的时候,最好有所关联
filter 方法并没有更新当前的数组而是创建了一个新数组,它会过滤掉不符合条件的数据项,符合条件的数据项就会组成新的数组。
filter 是 JavaScript 中删除数组元素的首选方法。
现在我们需要将removeUser传递给组件,并且在每一个列表行上渲染一个按钮,点击这个按钮调用这个方法。我们通过属性的方式将removeUser方法传递给 Table 组件。
TableBody 中,在调用removeItem()时,将数据对应的索引号(index)作为参数传递给了removeItem()方法,这样过滤方法就知道该删除哪个数据项了。创建一个按钮,并在onClick中调用removeItem().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function App() {
const [users, setUsers] = useState([{ name: "aaa" }]);
const removeUser = (index) => {
setUsers(users.filter((use, i) => index !== i));
};
return (
<div className="app">
<Table items={users} removeItem={removeUsers} />
</div>
);
}
function Table(props) {
const { items, removeUsers } = props;
return (
<table>
<TableHeader />
<TableBody items={items} removeItem={removeItem} />
</table>
);
}
function TableBody(props) {
const { items, removeUsers } = props;
const rows = items.map((item, index) => (
<tr key={index}>
<td>{items.name}</td>
<td>{items.job}</td>
<td>
<button onClick={() => removeUSer(index)}></button>
</td>
</tr>
));
return <tbody>{rows}</tbody>;
}

注意

组件传值的时候不要直接把传递过来的函数放到组件上,建议另写一个函数调用的时候带上这个props传递的函数.
下面的handleSubmit是从props拿到的,但是又封装一个函数才进行调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const Form = (props) => {
const initalData = {
name: "",
age: null,
};
const [formData, setFormData] = useState(initalData);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const submitForm = () => {
props.handleSubmit(formData);
setFormData(initalData);
};

return (
<>
<form>
<label>姓名</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
/>
<label>年龄</label>
<input
type="text"
name="age"
value={formData.age}
onChange={handleChange}
/>
<button onSubmit={submitForm}>提交</button>
</form>
</>
);
};

兄弟通信

状态提升

当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的  state  数据提升至其共同的父组件当中保存。之后父组件可以通过  props  将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。

  • 将共享状态提升到最近的公共父组件,由公共父组件管理状态
  • 思想: 状态提升
  • 公共父组件职责: 提供共享状态,提供操作共享状态的方法
  • 要通讯的子组件只需要通过props接收或者操作状态的方法

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Parent extends React.Component {
//共享状态
state = {
count: 0
}
//共享方法
const onIncrement = () => {
this.setState({
count += 1
})
}
render() {
return (
<div>
<Child1 count={this.state.count} />
<Child2 onIncrement={this.onIncrement} />
</div>
)
}
}
//子组件
const Child1 = props => {
return (
<div>计数器: {props.count} </div>
)
}

const Child2 = props => {
return (
<button onClick={() =>props.onIncrement()}>+1</button>
)
}

命名规范

因为 DOM 元素   是一个内置组件,因此其 onClick 属性在 React 中有特殊的含义。而对于用户自定义的组件来说,命名就可以由用户自己来定义了。我们给 Square 的 onClick 和 Board 的 handleClick 赋予任意的名称,代码依旧有效。在 React 中,有一个命名规范,通常会将代表事件的监听 prop 命名为 on[Event],将处理事件的监听方法命名为 handle[Event] 这样的格式。

函数式组件

将 class 组件修改为函数式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// class Square extends React.Component {
// render() {
// return (
// <button
// className="square"
// onClick={()=> this.props.onClick()}>
// {this.props.value}
// </button>
// );
// }
// }
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}

注意:
onClick={()=>this.props.onClick()} 
修改为
onClick={props.onClick} 
两侧括号消失

Props

  • 可以给组件传任意类型的值
  • props是只读的对象,只能读取属性的值,无法修改
  • 使用类组件时,如果写了构造函数,应该将props传递给super(),否则无法在构造函数中获取props

props 深入

children 属性

表示组件标签的子节点.当组件标签有子节点时,props 就会有该属性.
children 和普通的 props 属性一样,值可以是任意值.

1
2
3
4
5
6
7
8
9
10
11
12
const Test = () => <button>子组件</button>
const App = () => {
return (
<div>{props.children}</div>
)
}
ReactDOM.render(
<App>
{Test />
</App>,
document.getElementById("root")
)

props 校验

安装包prop-types,引入包,
使用组件名.propTypes={}来给组件的 props 添加校验规则.
校验规则通过PropTypes对象来指定

1
2
3
4
5
6
7
8
9
import PropTypes from "prop-types";
function App(props) {
return <h1>{props.colors}</h1>;
}
App.propTypes = {
//约定colors属性为array类型
//如果类型不对,则报错
colors: PropTypes.array,
};

约束规则

  1. 常见类型: array, bool, func, number, object, string
  2. React 元素类型: element
  3. 必填项: isRequired
  4. 特定结构的对象: shape({})
1
2
3
4
5
6
7
8
9
//常见类型
optionalFunc: PropTypes.func,
//必选
requiredFunc: PropTypes.func.isRequired,
//特定结构对象
optionalObjectwithShape: PropTypes.shape({
color: Proptypes.string,
fontSize: PropTypes.number
})

props 默认值

组件.defaultProps: 给组件传入默认值,在未传入 props 时生效.

State

不要直接修改 state,使用 this.setState()

1
2
3
4
5
//错误
this.state.comment = "hello";

//正确
this.setState({ comment: "hello" });

构造函数是唯一可以给 this.state  赋值的地方.

setState

setSate 接收一个函数,而函数的参数就是当前 state 的值.变化后记得将新的 state 返回出去.

1
this.setState((count) => count + 1);

state 的更新可能异步

this.props  和 this.state  可能会异步更新,不要依赖他们的值更新下一个状态.

1
2
3
4
//错误示例
this.setState({
counter: this.state.counter + this.props.increment,
});

正确解决方法:

setState  接收一个函数而不是一个对象.
这个函数用上一个 state  作为参数,将此次更新被应用时的 props  作为第二个参数:

1
2
3
4
this.setState({
(state, props) => ({
count: state.counter + props.increment
}))

state 的更新可能会被吞并

当调用 setState  时, React  会把提供的对象合并到当前的 state 中.

setState 是同步还是异步

分为两种时期的两种模式。
React17 之前的 legacy mode,和 react17 之后的 concurrent mode。

1
2
3
4
5
import ReactDOM from ‘react-dom’;
import App from './App';

const rootElement = document.getElementById('root');
ReactDOM.render(<App />,rootElement); // ReactDOM.render()就是开启legacy mode的方法

Legacy mode: 特点是同步执行。
分成两种情况:

  1. React 流程的 setState。如生命周期函数,React 事件响应函数。
  2. React 控制之外的函数。定时器,DOM 事件,Promise。

在 React 流程内,setState 是批量延后执行,也就看成了异步。React 将 setState 的各种更新先不更新,在生命周期的最后阶段才合成为一个对象,批量更新。

1
2
3
4
5
6
7
8
import ReactDOM from ‘react-dom’;
import { crreateRoot } from 'react-dom/client';

import App from './App';

const rootElement = document.getElementById('root')
const root = createRoot(rootElement)
root.render(<App />);

Concurrent Mode:并发模式。
即将 render 操作拆成一个个小的任务,异步执行。

Context

跨组件通信,也就是不同层级的组件访问相同的数据,比如当前的登录用户,主题,语言.

使用方法

使用React.createContext创建ProviderConsumer两个组件

1
const { Provider, Consumer } = React.createContext();

使用Provider作为父节点.

1
2
3
4
5
<Provider>
<div className="App">
<Child />
</div>
</Provider>

使用value属性表示要传递的数据.

1
<Provider value="red">

调用Consumer组件进行接收.Consumer 内部需要一个函数作为子元素.
即 data 接收的就是 value 的值.

1
<Consumer>{(data) => <span>data参数表示接收到的数据--{data}</span>}</Consumer>

关于 Provider 渲染

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

Class.contextType

挂载在 class 上的 contextType 属性可以赋值为 context 对象,此属性可以让我们通过this.context来获取 Context 的值.可以在任何生命周期中访问.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;

额外的

如果想避免层层传递某些属性,也可以使用组件组合()

数据是向下流动的

任何的  state  总是所属于特定的组件,而且从该  state  派生的任何数据或  UI  只能影响树中“低于”它们的组件。
如果把一个以组件构成的树想象成一个  props  的数据瀑布的话,那么每一个组件的  state  就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。

事件处理

不能通过返回 false  阻止事件默认行为,必须显示的使用 preventDefault .

this 的绑定

将 this 绑定到构造函数上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {
isToggleOn: true,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
isToggleOn: !state.isToggleOn;
});
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? "ON" : "OFF"}
</button>
);
}
}
ReactDom.render(<Toggle />, document.getElementById("app"));

不想 bind  的解决方法.

  1. 使用箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Toggle extends React.Component {
...
handleClick = ()=> {
this.setState(state =>{
isToggleOn: !state.isToggleOn
})

render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
  1. 在 render 中使用箭头函数
1
2
3
4
5
6
7
8
render(){
return (
<button onClick={()=> {this.handleClick()}>
Clcik me
</button>
)
}

向事件处理程序传递参数

若 id 是要删除的哪一行

1
2
<button onClick={(e)=>this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

关于 Click 中的箭头函数问题

1
2
3
4
5
6
7
onClick={这里是一个函数或函数引用}
onClick={() => this.handleClick(i)},这里面就是一个匿名函数,
//点击事件发生时,会执行这个匿名函数,匿名函数再调用handleClick函数(传参i);
//其次才是this绑定的问题

// 如果直接写函数执行,是错误的,因为会在页面渲染时立即执行,而不是点击才执行
onClick={handleClick()} // error

所以,如果要传递参数时,才需要使用箭头函数,或者绑定 this.

子组件的箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
<Panel handleClick={handleClick} handleResult={() => handleResult()} />;

const handleClick = (arg: string) => {};
const handleResult = (reverse = options.reverse) => {};
/*
上面如果handleClick={()=>handleClick()}或者
handleResult={handleResult}都是错的。
因为参数的问题,click参数是必传的,写成箭头函数就必须写参数,并且在子组件里是写了
handleClick={()=>handleClick(item)},所以外部也不能写箭头函数。
result参数可以不必传,如果不写箭头函数,那么就默认是react合成事件,
reverse实际是合成事件的值
*/

条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up</h1>;
}
function Greeting(props) {
const isLoginIn = props.isLoginIn;
if (isLoginIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}

ReactDOM.render(<Greeting isLoginIn={false} />, document.getElementBId("root"));

列表和 Key

一个元素的  key  最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的  id  来作为元素的  key.当元素没有确定  id  的时候,万不得已你可以使用元素索引  index  作为  key.
Vue不同,key写在返回式中.

map()

使用 map() 进行遍历时,记得 return .

1
2
3
4
5
let numbers = [1,2,3]
numbers.map(number => <li key={number.toString()}>{number}</li>)

//或者
numbers.map(number =>{return <li key={number.toString()}>{number}</li>)

表单

暂时空置

状态提升

在  React  应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state  都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个  state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步  state。

component composition 这种透传数据的模式

引用官网的一句话

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。
我们把我们需要用到数据的那个组件直接丢到数据来源的 props 身上 ,然后消费数据,把消费完的组件,也就是要被渲染到页面的内容,通过 props 传回来。这就是 component compositon ,简单粗暴,我们在原来的地方,直接渲染这个组件即可
例如:我们在 Page 组件中需要传递个 Auth 组件 user 信息,它们之间有很多的深层嵌套
我们可以这么做 (官网例子)

1
2
3
4
5
6
7
8
9
10
11
// 这里真正需要user和avatarSize的其实只有Link组件
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
//...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这里就是把上面的Link组件提出来,把组件通过props传递下去
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...Page的子组件
<PageLayout userLink={...} />
// ... 渲染出 ...PageLayout的子组件
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这样我们只用传递 userLink 即可,

组合 VS 继承

包含关系

有些组件无法提前知晓它们子组件的具体内容。在  Sidebar(侧边栏)和  Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。
我们建议这些组件使用一个特殊的  children prop  来将他们的子组件传递到渲染结果中.

1
2
3
4
5
6
7
function FancyBorder(props) {
return (
<div className={"FancyBorder FancyBorder-" + props.color}>
{props.children}
</div>
);
}

JSX 标签中的所有内容都会作为一个 childrenprop 传递给 FancyBorder 组件。因为 FancyBorder 将 {props.children}  渲染在一个

少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children ,而是自行约定:将所需内容传入 props ,并使用相应的 prop 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Contacts() {
return <div className="Contacts" />;
}
function Chat() {
return <div className="Chat" />;
}
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">{props.left}</div>
<div className="SplitPane-right">{props.right}</div>
</div>
);
}

function App() {
return <SplitPane left={<Contacts />} right={<Chat />} />;
}
ReactDOM.render(<App />, document.getElementById("root"));

特例关系

暂无

生命周期

创建时

constructor=>render()=>componentDidMount

生命周期 触发时机 作用
constructor 创建组件 1.初始化 state
2.为事件处理程序绑定 this
render 每次渲染都会触发 渲染 UI(注意不能调用 setState)
componentDIdMount 组件挂载(完成 DOM 渲染后) 1.发送网络请求
2.DOM 操作

更新时

执行时机: 1.setState2.forceUpdate()3.组件接收新的 props
执行顺序: render() => componentDidUpdate

钩子函数 触发时机 作用
render 每次组件渲染都会触发 渲染 UI,与挂载阶段是同一个 render
componentDidUpdate 组件更新(完成 DOM 渲染后) 1.发送网络请求

2.DOM 操作
注意如果要 setState 必须放在 if 条件中 |

1
2
3
4
5
6
7
//如果要在componentDidUpdate中setState,需要加if
componentDidUpdate(prevProps) {
//比较更新前后的props是否相同,决定是否重新渲染
if(prevProps.count !== this.props.count) {
this.setState({})
}
}

卸载时

钩子函数 触发时机 作用
componentWillUnmount 组件卸载 执行清理工作(如卸载定时器)
1
2
3
4
5
6
7
8
componentDidMount() {
this.timeId = setInterval(() => {
console.log("定时器执行")
},1000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}

其他

shouldComponentUpdate

组件

组件复用

要复用什么? 1.state2.操作state的方法(组件状态逻辑)
方式: 1.render props模式 2.HOC高阶组件 3. 自定义 hooks

render props

思路: 将复用的 state 和操作 state 的方法封装到一个组件
问题 1: 如何拿到该组件中复用的 state?
方法: 在使用组件时,添加一个值为函数的 prop,通过函数参数来获取(需要组件内部实现)
问题 2: 如何渲染任意 UI?
方法: 使用该函数的返回值作为要渲染的内容.(需要组件内部实现)

render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
1
2
3
4
<Mouse render={(mouse) => {}} />
<Mouse render={(mouse) => (
<p>鼠标当前位置:{mouse.x},{mouse.y}</p>
) }/>
具体步骤:
  1. 创建 Mouse 组件,在组件中提供复用的状态逻辑代码
  2. 将要复用的状态作为props.render(state)方法的参数,暴露到组件外部
  3. 使用props.render()的返回值作为要渲染的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Mouse extends React.Component {
state = {
x = 0,
y = 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: clientY
})
}
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 这里是关键
render() {
return this.props.render(this.state)
}
}

class App extends React.Component {

render() {
return (
<div>
<Mouse render={(mouse) => {
return (
<p>鼠标位置: {mouse.x}, {mouse.y}</p>
)
}} />
<Mouse render={(mouse) => {
return (
<img src={img} alt="cat" style={{
postion: 'absolute',
top: mouse.y,
left: mouse.x
}}
</div>
)
}
}

使用 children 替代 render

1
2
3
4
5
6
7
<Mouse>
{ {(x,y)} => <p>鼠标的位置: {x},{y}</p> }
</Mouse>
// 组件内部,见上面
render(){
return this.props.children(this.state)
}

代码优化

  1. 使用 propTypes 进行校验
  2. 将事件绑定解除
1
2
3
4
5
6
Mouse.propTypes = {
children: PropTypes.func.isRequired
}
componentWillUnmount() {
window.removeEventListener('mousemove',this.handleMouseMove)
}

HOC 高阶组件

目的: 实现逻辑复用
方法: 采用包装(装饰)模式
思路: 实际上是一个函数,接收要包装的组件,返回增强后的组件
方法: 内部创建一个类组件,在类组件内提供复用的状态逻辑代码,通过 prop 将复用的状态传递给被包装的组件.
使用场景: 比如暗黑模式.将组件进行包裹.

使用步骤

  1. 创建一个函数,名称约定以with开头.
  2. 指定函数参数,参数以首字母大写开头
  3. 在函数内部创建一个类组件,提供复用的状态逻辑代码
  4. 在该组件,渲染参数组件,同时将状态通过 prop 传递给参数组件
  5. 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React from 'react';
//声明高阶组件
function withMouse(wrappedComponent) {
//创建类组件
class Mouse extends React.Component {
//在这里提供提供复用的代码和state
state = {
x = 0,
y = 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: clientY
})
}
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
//将内部的state放到包裹的组件上
return <wrappedComponent {...this.state} />
}
}
//返回出这个类
return Mouse

}

const Position = props => {
<p>鼠标位置: {props.x}, {props.y}</p>
}
const Cat =(props) => (
<img src={img} alt="cat" style={{
postion: 'absolute',
top: props.y,
left: props.x
}} />
)

//使用高阶组件
const MousePosition = withMouse(Position)
const MouseCat = withMouse(Cat)

class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
<MousePosition />
<MouseCat />
</div>
)
}
}

设置 display Name

原因: 默认情况,React 使用组件名作为 displayName,则会出现两个名字相同的情况
方法: 设置 displayName,便以区分.注意名字首字母大写.

1
2
3
4
5
6
7
8
9
10
11
12
function withMouse(wrappedComponent) {
class Mouse extends React.Component {
//...
}
//在高阶组件内设置displayName,注意首字母大写
Mouse.displayName = `WithMouse${getDisplayName(wrappedComponent)}`;
return Mouse;
}
//设置获取名称方法,注意首字母大写
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}

传递 props

将 props 和 state 一起放在返回的组件上

1
return <wrappedComponent {...state} {...props} />;

DOM 元素

渲染文章时防止 XSS 攻击,使用特殊的写法dangerouslySetInnerHTML={{__html:'文章内容'}}

1
2
3
4
5
6
<Content>
{contentData &&
contentData.map((item, index) => (
<p key={index} dangerouslySetInnerHTML={{ __html: item }}></p>
))}
</Content>

React 合成事件

  • jsx 上写的事件没有绑定在真实 DOM 上,而是通过事件代理的方式绑定在 document 上.
  • 冒泡到 document 上的也不是原生事件,而是合成事件.如果不想冒泡,需要使用event.preventDefault,不能使用event.stopPropagation.
  • 合成事件是有个专门的事件池来管理它们的创建和销毁.当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

React 合成事件和 dom 原生事件的顺序

  • react 的所有事件都挂载在 document 中
  • 当真实 dom 触发后冒泡到 document 后才会对 react 事件进行处理
  • 所以原生的事件会先执行
  • 然后执行 react 合成事件
  • 最后执行真正在 document 上挂载的事件

React 事件系统

React 的事件系统分为几个部分:

  • 事件注册;
  • 事件监听;
  • 事件合成;
  • 事件派发;

事件系统流程:
在 React 代码执行时,内部会自动执行事件的注册;
第一次渲染,创建 fiberRoot 时,会进行事件的监听,所有的事件通过 addEventListener 委托在 id=root 的 DOM 元素上进行监听;
在我们触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象;
最后进行事件的派发,执行我们代码中的事件回调函数;

新项目启动

1
2
3
4
// 全局安装taro
npm i -g @tarojs/cli
// 项目初始化
taro init appName

微信小程序

需要下载微信小程序开发工具,使用工具打开 taro 项目根目录。

老项目更新依赖

老项目在新 taro 框架,需要更新

1
taro update project

Taro3 和老版本引入有差别

引入

1
2
3
4
5
6
//老版本
import Taro, { Component } from "@tarojs/taro";

//Taro3新版本
import Taro from "@tarojs/taro";
import { Component } from "react";

挂载 App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//老版本app.js
import Taro, { Component } from "@tarojs/taro";
import Index from "./pages/index";
import "./app.css";

class App extends Component {
//config写到app的类里.
//新版写到单独的app.config.js文件里
config = {
pages: [
"pages/index/index",
"pages/nodes/nodes",
"pages/hot/hot",
"pages/node_detail/node_detail",
"pages/thread_detail/thread_detail",
],
tabBar: {
list: [
{
iconPath: "resource/latest.png",
selectedIconPath: "resource/lastest_on.png",
pagePath: "pages/index/index",
text: "最新",
},
{
iconPath: "resource/hotest.png",
selectedIconPath: "resource/hotest_on.png",
pagePath: "pages/hot/hot",
text: "热门",
},
{
iconPath: "resource/node.png",
selectedIconPath: "resource/node_on.png",
pagePath: "pages/nodes/nodes",
text: "节点",
},
],
color: "#000",
selectedColor: "#56abe4",
backgroundColor: "#fff",
borderStyle: "white",
},
window: {
backgroundTextStyle: "light",
navigationBarBackgroundColor: "#fff",
navigationBarTitleText: "V2EX",
navigationBarTextStyle: "black",
},
};
//return的是首页.
//新版本return的是this.prop.children
render() {
return (
<Index />
// return this.props.children
);
}
}
//老版本使用Taro.render挂载到app节点上
//新版本直接export default App

Taro.render(<App />, document.getElementById("app"));
//export default App

按需引入

taro-UI  的样式可以按需引入.

研究多 tab 页报错问题,路径地址写错了

路由

跳转

小程序的跳转Taro.navigateTo.
H5 的跳转是从@tarojs/router中引入navigateTo.

路由声明

路由在 @tarojs/taro  中的 getCurrentInstance().router 中获取.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { getCurrentInstance } from "@tarojs/taro";
class C extends Component {
current = getCurrentInstance();

componentWillMount() {
// getCurrentInstance().router 和 this.$router 和属性一样
console.log(this.current.router);
}
}

// 函数式组件
import { getCurrentInstance } from "@tarojs/taro";
function C() {
const { router } = getCurrentInstance();
// getCurrentInstance().router 和 useRouter 返回的内容也一样
// const router = useRouter()
}

路由参数获取

原生小程序路由一般在 onLoad  中获取传过来的参数.
Taro  推荐在 ComponentDidShow  中获取.(也可以在 onLoad  中)

1
2
3
4
5
6
7
8
9
// app.js 项目入口文件
class App extends Component {
componentDidShow (options /* 这里有你想要的路由信息 */) {
}

render () {
...
}
}

生命周期

小程序 => class 类 => 函数式 hook
onLoad => onLoad
onReady => onReady => useReady
onLanch => onLanch
onShow => conponentDidShow => useDidShow
onHide =>componentDidHide => useDidHide

事件

onPullDownRefresh()

监听用户下拉动作。

需要在全局配置的  window  选项中或页面配置中设置  enablePullDownRefresh: true。
可以通过  Taro.startPullDownRefresh  触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。
当处理完数据刷新后,Taro.stopPullDownRefresh  可以停止当前页面的下拉刷新.

onReachBottom()

监听用户上拉触底事件。

可以在全局配置的  window  选项中或页面配置中设置触发距离  onReachBottomDistance。
在触发距离内滑动期间,本事件只会被触发一次

H5  暂时没有同步实现,可以通过给  window  绑定  scroll  事件来进行模拟

onPageScroll()

监听用户滑动页面事件.

H5  暂时没有同步实现,可以通过给  window  绑定  scroll  事件来进行模拟

基本事件

在  Taro  中事件遵从小驼峰式(camelCase)命名规范,所有内置事件名以  on  开头。

在事件回调函数中,第一个参数是事件本身,回调中调用  stopPropagation  可以阻止冒泡。

只有小程序的  bindtap  对应  Taro  的  onClick
其余小程序事件名把  bind  换成  on  即是  Taro  事件名

Redux

安装 redux 及其中间件

1
2
3
$ yarn add redux react-redux redux-thunk redux-logger
# 或者使用 npm
$ npm install --save redux react-redux redux-thunk redux-logger

随后可以在项目  src  目录下新增一个  store  目录,在目录下增加  index.js  文件用来配置  store,按自己喜好设置  redux  的中间件,例如下面例子中使用  redux-thunk  和  redux-logger  这两个中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { createStore, applyMiddleware, compose } from "redux";
import thunkMiddleware from "redux-thunk";
import rootReducer from "../reducers";

const composeEnhancers =
typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
})
: compose;

const middlewares = [thunkMiddleware];

if (
process.env.NODE_ENV === "development" &&
process.env.TARO_ENV !== "quickapp"
) {
middlewares.push(require("redux-logger").createLogger());
}

const enhancer = composeEnhancers(
applyMiddleware(...middlewares)
// other store enhancers if any
);

export default function configStore() {
const store = createStore(rootReducer, enhancer);
return store;
}

接下来在项目入口文件  app.js  中使用  redux  中提供的  Provider  组件将前面写好的  store  接入应用中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Component } from "react";

// 假设我们要使用 Redux
import { Provider } from "react-redux";
import configStore from "./store";

// 全局样式
import "./app.css";

const store = configStore();

class App extends Component {
// 可以使用所有的 React 组件方法
componentDidMount() {}

// 对应 onLaunch
onLaunch() {}

// 对应 onShow
componentDidShow() {}

// 对应 onHide
componentDidHide() {}

render() {
// 在入口组件不会渲染任何内容,但我们可以在这里做类似于状态管理的事情
return (
<Provider store={store}>
/* this.props.children 是将要被渲染的页面 */
{this.props.children}
</Provider>
);
}
}

export default App;

然后就可以开始使用了。如  redux  推荐的那样,可以增加

  • constants  目录,用来放置所有的  action type  常量
  • actions  目录,用来放置所有的  actions
  • reducers  目录,用来放置所有的  reducers

Koa 本质上是调用一系列的中间件,来处理对应的请求,并决定是否传递到下一个中间件去处理。

1
npm install koa --save

所以,接口的本质,就是判断不同的请求链接,干不同的事情,返回相应的结果。那么我们得需要一个路由中间件来处理分发请求。
使用 koa-router  处理路由.

1
npm install koa-router --save
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const Koa = require("koa");
const app = new Koa();
const Router = require("koa-router");
const router = new Router();

//地址跳转
router.get("/", (ctx) => {
ctx.body = "这是主页";
});
router.get("/user", (ctx) => {
ctx.body = "这是user页";
});
//这里koa不会去处理数据,需要使用中间件body-parse
router.get("/post", (ctx) => {
ctx.body = ctx.request.body;
});
router.get("async", async (ctx) => {
const sleep = async (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, ms);
});
};
await sleep(1000);
ctz.body = `这是异步网页`;
});

app.use(router.routes()).use(router.allowedMethods());

// app.use(ctx => {
// ctx.body = `您的网址为:${ctx.request.url}`
// })

app.listen(3000);

安装 body-parse .

1
npm install koa-bodyparser --save

业务分离

改造后目录结构

1
2
-koa2 - node_modules - controller;
user.js - index.js - router.js - package.json;
1
2
3
4
5
6
7
8
9
10
11
//index.js
const Koa = require("koa");
const app = new Koa();
const router = require("./router");
const bodyParser = require("koa-bodparser");

app.use(bodyParser);

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

拉取镜像
docker pull image 
根据镜像创建使用该镜像的容器
docker run -di --name mycentos centos:7

Docker 常用命令

当前运行的容器
docker ps

Dockerflie

docker image build  会读取 dockerfile,并将应用容器化.
Dockerfile  由一行行命令语句组成,并支持以  #  开头的注释行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Test web-app to use with Pluralsight courses and Docker Deep Dive book
# Linux x64
FROM alpine

LABEL maintainer="nigelpoulton@hotmail.com"

# Install Node and NPM
RUN apk add --update nodejs nodejs-npm

# Copy app to /src
COPY . /src

WORKDIR /src

# Install dependencies
RUN npm install

EXPOSE 8080

ENTRYPOINT ["node", "./app.js"]

使用  -t  参数为镜像打标签,使用  -f  参数指定  Dockerfile  的路径和名称,使用  -f  参数可以指定位于任意路径下的任意名称的  Dockerfile。

构建上下文是指应用文件存放的位置,可能是本地  Docker  主机上的一个目录或一个远程的  Git  库。

Dockerfile  中的  FROM  指令用于指定要构建的镜像的基础镜像。它通常是  Dockerfile  中的第一条指令。

Dockerfile  中的  RUN  指令用于在镜像中执行命令,这会创建新的镜像层。每个  RUN  指令创建一个新的镜像层。

Dockerfile  中的  COPY  指令用于将文件作为一个新的层添加到镜像中。通常使用  COPY  指令将应用代码赋值到镜像中。

Dockerfile  中的  EXPOSE  指令用于记录应用所使用的网络端口。

Dockerfile  中的  ENTRYPOINT  指令用于指定镜像以容器方式启动后默认运行的程序。

其他的  Dockerfile  指令还有  LABEL、ENV、ONBUILD、HEALTHCHECK、CMD  等。