JS 知识点
待补充的:Promise
的使用、class
的使用、JS设计模式
# 1 ES6,ES6 新特性
ES6 即 ECMAScript 6,也称 ES2015,是 2015 颁布的 JS 标准。ECMA 是一个国际化标准组织,JS 被提交到这里。
主要的新功能有:
- 新语法,如加入
let
、const
,扩展运算符和剩余参数...
,解构,模板字面量,Symbol
等 - 代码模块化,支持
export
和import
- 数字、字符串、正则新特性,如提供十六进制、添加
Number.isNaN()
(减少全局转换)、字符串includes()
、repeat()
函数等 - 对象、数组新特性,如对象支持重复属性、数组支持
Array.of()
、Array.from()
、引入fill()
等新方法 - 函数新特性,如箭头函数、默认参数等
- 新增
Set
和Map
数据结构 - 新增迭代器和生成器,新增
for-of
循环 - 支持
class
直接生成类 - 新增
Promise
- 新增代理、反射
# 2 JS 数据类型
基本类型:number
、boolean
、string
、null
、undefined
、symbol
、bigint
。
引用类型:object
,注意 array
、function
也是对象。
# 2.1 typeof
可以使用 typeof
判断数据类型,它的返回值有:"number"
、"boolean"
、"string"
、"undefined"
、"symbol"
、"bigint"
、"object"
、"function"
。
NaN
、Infinity
的typeof
返回"number"
- 未定义的变量做
typeof
,不会报错,而返回"undefined"
null
的typeof
返回"object"
,array
的typeof
返回"object"
(判断array
可用Array.isArray()
),基本类型的包装类对象的typeof
返回"object"
class
的typeof
返回"function"
对于函数、自定义类,需要用 instanceof
判断。
console.log(typeof 1); // number
console.log(typeof Infinity); // number
console.log(typeof NaN); // number
console.log(typeof true); // boolean
console.log(typeof "1"); // string
console.log(typeof undefined); // undefined
console.log(typeof x); // undefined,未定义变量
console.log(typeof null); // object
console.log(typeof Symbol("a")); // symbol
console.log(typeof 1n); // bigint
console.log(typeof {}); // object
console.log(typeof []); // object,数组也是对象
console.log(typeof new Number(1)); // object,复杂原始类型
console.log(typeof function() {}); // function
console.log(typeof class A {}); // function,类也是函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.2 instanceof
instanceof
判断复杂原始类型、object
、自定义数据类型。
- 简单原始类型只能用
typeof
,复杂原始类型只能用instanceof
function
用typeof
、instanceof
均可- 对象、自定义类型用
typeof
均返回"object"
,用instanceof
可判断原型
console.log(typeof 1); // number
console.log(1 instanceof Number); // false
console.log(typeof new Number(1)); // object
console.log(new Number(1) instanceof Number); // true
console.log(typeof /a/); // object,正则表达式也是对象
console.log(/a/ instanceof RegExp); // true
console.log(typeof {}); // object
console.log({} instanceof Object); // true
console.log(typeof []); // object
console.log([] instanceof Array); // true
function Teacher() {}
function Student() {}
const teacher = new Teacher();
console.log(typeof teacher); // object
console.log(teacher instanceof Teacher); // true
console.log(teacher instanceof Student); // false
console.log(teacher instanceof Object); // true,teacher -> Teacher.prototype -> Object.prototype
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3 手写 instanceof
循环使用 __proto__
或 Object.getPrototypeOf()
。
function instanceOf(a, b) {
if (typeof a !== 'object' && typeof a !== 'function') {
return false; // 确保a不是基本类型
}
a = a ? a.__proto__ : null;
while (true) {
if (a === null) {
return false;
}
if (a === b.prototype) {
return true;
}
a = a.__proto__;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.4 Object.prototype.toString.call()
将 Object.prototype.toString()
用于其他变量,可以得到形如 "[object xxx]"
的字符串。
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call("1")); // [object String]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(Symbol("a"))); // [object Symbol]
console.log(Object.prototype.toString.call(1n)); // [object BigInt]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(function() {})); // [object Function]
2
3
4
5
6
7
8
9
10
# 2.5 constructor
由于对象的原型(即 __proto__
属性指向的对象)有一个 constructor
属性指向构造函数,因此对象也可以调用它。基本类型也可以调用。
console.log((1).constructor === Number); // true
console.log(true.constructor === Boolean); // true
console.log("1".constructor === String); // true
// console.log(null.constructor); // TypeError: Cannot read property 'constructor' of null
// console.log(undefined.constructor); // TypeError: Cannot read property 'constructor' of undefined
console.log(Symbol("a").constructor === Symbol); // true
console.log(1n.constructor === BigInt); // true
console.log([].constructor === Array); // true
console.log({}.constructor === Object); // true
console.log(function() {}.constructor === Function); // true
console.log(new Date().constructor === Date); // true
console.log(/a/.constructor === RegExp); // true
class Teacher {}
console.log(new Teacher().constructor === Teacher); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3 变量声明、变量提升、作用域
# 3.1 var
、let
、const
的区别
const
除定义外不可重新赋值、定义时必须给定初始值外,和 let
并无不同。
注意 var
的作用域是 函数作用域,let
、const
的作用域是 块作用域。
- 声明提升:
var
定义的变量会被提升到代码最顶端(赋值不提升),let
不会提升(实际上会提升并形成暂时性死区) - 重复声明:
var
定义的变量可以重复声明(甚至可以不加var
声明,这时会突破函数作用域成为全局变量),let
不允许重复声明(不同作用域下可以) - 全局覆盖:
var
定义的变量会成为全局变量及全局对象的属性,let
不会覆盖全局对象的属性
在 循环 中,let
不会出现异步回调问题,因为会创建同名变量;const
常用于 for-in
。
几个细节:
var
定义的变量函数外是否可访问? 不可访问,var
是函数作用域。var
定义的变量与function
声明的函数谁先提升? 函数优于变量提升,同名变量提升不会覆盖函数提升,函数声明提升而表达式不提升。如何证明
let
声明的变量也进行了提升? 在函数中先使用全局let
变量,再声明同名let
变量,会发现不可访问,即形成暂时性死区。
# 3.2 setTimeout
中的 var
和 let
var
:setTimeout
回调会在循环结束后再执行,输出var
变量的最终值let
:每次遍历时i
都创建了同名变量,输出每次let
变量的循环值
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3 3 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j)); // 0 1 2
}
var x = 0;
for (let y = 0; x < 3 && y < 3; x++, y++) {
setTimeout(() => console.log(`${x},${y}`)); // 3,0 3,1 3,2
}
2
3
4
5
6
7
8
9
10
# 3.3 变量提升、函数声明提升
所有带 var
声明都会被提升到 函数作用域 最顶端;重复声明的变量只提升一次;非 var
声明的全局变量不提升。
函数声明也提升且优于变量提升;与函数同名的变量提升不覆盖函数;函数表达式不会提升。
情况 1:
var a = true; fun(); function fun() { if (a) { var a = 10; } console.log(a); }
1
2
3
4
5
6
7
8等同于:
function fun() { var a; if (a) { a = 10; } console.log(a); // undefined } var a; a = true; fun();
1
2
3
4
5
6
7
8
9
10
11function fun()
和var a
提升,fun()
内部的var a
也提升。情况 2:
fun(); var fun = function () { console.log("var"); } fun(); function fun() { console.log("fun"); } fun();
1
2
3
4
5
6
7
8
9等同于:
function fun() { console.log("fun"); } var fun; fun(); // fun fun = function () { console.log("var"); } fun(); // var fun(); // var
1
2
3
4
5
6
7
8
9
10
11function fun()
和var fun
提升,var fun
不覆盖function fun()
。函数表达式被认为是赋值。情况 3:
var a = 1; function fn(){ var a = 2; function a(){console.log(3);} return a; function a(){console.log(4);} } var b = fn(); console.log(b);
1
2
3
4
5
6
7
8
9等同于:
function fn(){ function a(){console.log(3);} function a(){console.log(4);} var a; a = 2; return a; } var a; var b; a = 1; b = fn(); console.log(b); // 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14function fn()
、var a
、var b
依次提升,内部的两个function a()
和var a
依次提升。
# 3.4 作用域
全局作用域,函数作用域,块作用域(ES6 新增)。
var
在全局定义的变量为全局作用域,也成为全局对象的属性;不加 var
声明的变量会突破函数作用域成为全局变量(全局对象的属性)。
(function() {
var a = b = 5;
})();
var c = 55;
console.log(window.c); // 55
console.log(b); // 5
console.log(window.b); // 5
console.log(a); // ReferenceError: a is not defined
2
3
4
5
6
7
8
let
、const
定义的作用域为块级作用域,如 if{}
、for{}
等,甚至单纯的 {}
(对象字面量语句除外),同一块级作用域下变量不能重复声明。
(() => {
let x, y
try {
throw new Error()
} catch (x) {
(x = 1), (y = 2)
console.log(x) // 1
}
console.log(x) // undefined
console.log(y) // 2
})()
2
3
4
5
6
7
8
9
10
11
# 4 操作符
# 4.1 ==
和 ===
==
:基础类型执行数据转换(转为数值,null
和 undefined
不转),对象比较引用
===
:比较值和数据类型
console.log(null == undefined); // true
console.log("NaN" == NaN); // false
console.log(5 == NaN); // false
console.log(NaN == NaN); // false
console.log(NaN != NaN); // true
console.log(false == 0); // true
console.log(true == 1); // true
console.log(true == 2); // false
console.log(true == "1"); // true
console.log(undefined == 0); // false
console.log(null == 0); // false
console.log("5" == 5); // true
2
3
4
5
6
7
8
9
10
11
12
# 4.2 +
一元操作符可以作为 Number()
使用。
二元操作符注意只要有字符串,就执行字符串连接,否则强转为数字相加(Symbol
不可强转)。对于对象,调用 valueOf()
或 toString()
。
console.log(+true); // 1
console.log(1 + 2 + '3'); // 33
console.log(NaN + 1); // NaN,NaN不参与运算
console.log(Infinity + Infinity, -Infinity + -Infinity, Infinity + -Infinity); // Infinity, -Infinity, NaN,遵循数学规则
console.log(+0 + +0, -0 + -0, +0 + -0); // 0, -0, 0,遵循数学规则
console.log(true + undefined, true + null, false + undefined, false + null); // NaN, 1, NaN, 0,强转为数字
console.log([] + [], {} + {}, [] + {}); // '', [object Object][object Object], [object Object],强转为字符串
console.log({} + 1); // [object Object]1,调用toString方法
console.log({ valueOf: () => 1 } + 1); // 2,调用valueOf方法
console.log({ toString: () => 1 } + 1); // 2,调用toString方法
2
3
4
5
6
7
8
9
10
# 5 数字
JS 中的数字是 64 位表示的。
# 5.1 Number
类和全局的 isNaN
、isFinite
方法
全局的 isNaN()
和 isFinite()
期待接收一个 number
参数,如果参数非 number
会被先强转为 number
类型。
Number
类的 isNaN()
和 isFinite()
为 ES6 新增,期待接收一个 unknown
参数,它首先会判断参数是否为 number
类型,如果不是直接返回 false
,然后再判断该 number
类型是否是 NaN
或 Infinity
。
const name = "John";
const age = 30;
const points = "100";
const nan = NaN;
const infinity = Infinity;
console.log(isNaN(name), Number.isNaN(name)); // true false
console.log(isNaN(age), Number.isNaN(age)); // false false
console.log(isNaN(points), Number.isNaN(points)); // false false
console.log(isNaN(nan), Number.isNaN(nan)); // true true
console.log(isNaN(infinity), Number.isNaN(infinity)); // false false
console.log(isFinite(name), Number.isFinite(name)); // false false
console.log(isFinite(age), Number.isFinite(age)); // true true
console.log(isFinite(points), Number.isFinite(points)); // true false
console.log(isFinite(nan), Number.isFinite(nan)); // false false
console.log(isFinite(infinity), Number.isFinite(infinity)); // false false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.2 map
与 parseInt
问题
以下代码输出什么?
console.log(["1", "2", "3"].map(parseInt)); // [1, NaN, NaN]
map
回调(及其他几个类似的函数)完整的参数列表为:value
(当前值)、index
(当前索引)、array
(原数组)。
回调直接使用 parseInt
时,完整的参数列表会被传入 parseInt
,例如第一次 map
传入 ("1", 0, ["1", "2", "3"])
。
parseInt
最多可以接收两个参数,第一个参数是字符串,第二个参数表示进制(支持 2 到 36),会将字符串按对应的进制转换为十进制数字。当传入的进制为 0
时,默认为十进制。
因此,parseInt("1", 0)
返回 1
,parseInt("2", 1)
返回 NaN
,parseInt("3", 2)
返回 NaN
(二进制不存在数字 3
)。
如果想要实现预计的功能,可以使用 Number
代替 parseInt
。
console.log(["0", "1", "2"].map(Number)); // [0, 1, 2]
# 6 字符串
# 6.1 实现一个模板字符串替换,数据源为对象
let template = "你好,我们是{{company}},我们来自{{group}},我们有{{business[0]}}、{{business[1]}}等。"
let obj = {
company: "阿里",
group: "蚂蚁",
business: ["支付宝", "蚂蚁金服", "相互宝"]
}
2
3
4
5
6
7
先匹配所有的 {{*}}
字符串,然后逐一替换,可以,但不好。这里的正则表示:匹配形如 {{ 字母 [数字] }}
格式的字符串。
function render(template, obj) {
const arr = template.match(/{{[a-zA-Z\d]+(\[\d+\])?}}/g);
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i].replace(/{{|}}/g, "");
template = template.replace("{{" + arr[i] + "}}", eval("obj." + arr[i]));
}
return template;
}
2
3
4
5
6
7
8
直接在字符串中查找 {{*}}
并一行内替换。几个技巧:
.*?
是正则固定用法,表示非贪婪匹配,防止从第一个{{
到最后一个}}
都被匹配()
表示正则中的子表达式,可以用$1-$99
获取- 如果
{}
中间不包含数字(形如{1,2}
),则{}
本身不需要转义 replace
第二个参数支持一个回调函数,回调函数第一个参数是匹配结果,第二个参数是正则中的子表达式匹配结果(可以有 0 个或多个该参数),接下来的参数是匹配位置,最后的参数是原串本身
function render(template, obj) {
return template.replace(/{{(.*?)}}/g, (match, key) => eval("obj." + key));
}
2
3
有没有不用 eval
的方法?
匹配时就先拿到 {{*}}
内的字母与数字。
function render(template, obj) {
return template.replace(/{{(.*?)(\[(\d)*\])?}}/g, (match, key, index, num) => index ? obj[key][num] : obj[key]);
}
2
3
参考:
- https://wenku.baidu.com/view/15486d3f5c0e7cd184254b35eefdc8d376ee14d0.html (opens new window)
- https://corecabin.cn/2022/10/06/implement-a-template-string-replacement-and-the-data-source-is-an-object/ (opens new window)
# 6.2 字符串与正则的几个匹配方法区别
- 正则对象的方法:
RegExp.prototype.exec()
:接收一个字符串参数,返回一个包含index
、input
属性的数组。- 数组的元素为匹配项和圆括号捕获的组
- 对于
g
标记的正则,每次调用都会更新正则的lastIndex
,即开启下一次搜索 - 搜索不到结果时,返回
null
RegExp.prototype.test()
:接收一个字符串参数,返回是否匹配的布尔值
- 字符串的方法:
String.prototype.match()
:完全等同于exec
,接收一个正则对象或正则字符串- 注意在
g
模式下,match
返回所有匹配结果的数组,这与exec
不同
- 注意在
String.prototype.search()
:接收一个正则对象或正则字符串,返回第一个匹配位置的索引(没找到返回-1
)String.prototype.replace()
:接收两个参数,返回更新后的字符串- 第一个参数是正则对象或普通字符串(非正则)
- 第二个参数可以是一个字符串(可使用
$01-$99
获取捕获字符串),也可以是一个函数,函数参数分别是匹配结果、捕获项、匹配位置、原串,返回值是替代字符串
const text = "cat, bat, sat, fat";
const pattern = /(.)at/g;
console.log(pattern.exec(text)); // ["cat", "c", index: 0, input: "cat, bat, sat, fat"]
console.log(pattern.exec(text)); // ["bat", "b", index: 5, input: "cat, bat, sat, fat"]
console.log(pattern.test(text)); // true
console.log(text.match(pattern)); // ["cat", "bat", "sat", "fat"]
console.log(text.search(pattern)); // 0
console.log(text.replace(pattern, "word($1)")); // word(c), word(b), word(s), word(f)
2
3
4
5
6
7
8
# 7 数组
# 7.1 数组的哪些原型方法会/不会改变原数组,参数、返回值是什么
- 会改变数组的方法:
- 复制和填充方法。 如
copyWithin()
(从指定位置开始浅复制对应索引范围)、fill()
(填充指定值到对应索引范围),返回数组本身。 - 栈方法。 如
push()
(接收不定参数、返回数组最新长度)、pop()
(无参、返回数组最后一项)。 - 队列方法。 如
shift()
(无参、返回数组第一项)、unshift()
(接收不定参数、返回数组最新长度)。 - 排序方法。 如
sort()
、reverse()
,返回数组本身。 - 部分操作方法。 如
splice()
(传入起始索引、删除数目、添加元素,返回被删除元素数组)。
- 复制和填充方法。 如
- 不会改变数组的方法:
- 迭代器方法。 如
keys()
、values()
、entries()
,返回对应迭代器。 - 转换方法。 如
toLocaleString()
、toString()
、valueOf()
,返回字符串或数组本身。 - 部分操作方法。 如
concat()
(传入多个数组或非数组参数)、slice()
(传入首尾索引),返回一个新数组。 - 搜索和位置方法。 如
indexOf()
、lastIndexOf()
、find()
、findIndex()
,返回索引值或搜索元素。 - 迭代方法。 如
every()
、filter()
、forEach()
、map()
、some()
,返回布尔值、新数组或无返回值。 - 归并方法。 如
reduce()
、reduceRight()
,返回归并值。
- 迭代器方法。 如
# 7.2 数组静态方法
Array.of()
:接收不定参数,返回由不定参数返回的数组。注意Array.of(2)
和Array(2)
(或new Array(2)
)的区别。console.log(Array.of(1, 2, 3)); // [1, 2, 3] console.log(Array.of(2)); // [2] console.log(Array(2)); // [ <2 empty items> ]
1
2
3Array.from()
:接收一个可迭代参数,返回该可迭代参数转变的数组。例如Set
、Map
、字符串或生成器对象。const set = new Set([1, 2, 3]); const map = new Map([['a', 1], ['b', 2], ['c', 3]]); function* gen() { for (let i = 0; i < 3; i++) { yield i; } } console.log(Array.from(set)); // [1, 2, 3] console.log(Array.from(map)); // [['a', 1], ['b', 2], ['c', 3]] console.log(Array.from("hello")); // ['h', 'e', 'l', 'l', 'o'] console.log(Array.from(gen())); // [0, 1, 2]
1
2
3
4
5
6
7
8
9
10
11Array.isArray()
:判断一个对象是否为数组。
# 7.3 怎么判断数组
Array.isArray()
静态方法instanceof
- 调用对象
constructor
属性的name
- 借用
Object.prototype.toString()
方法,使其返回"[object Array]"
function isArray1(arr) {
return Array.isArray(arr);
}
function isArray2(arr) {
return arr instanceof Array;
}
function isArray3(arr) {
return arr.constructor.name === 'Array';
}
function isArray4(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
const arr = [1, 2, 3];
console.log(isArray1(arr)); // true
console.log(isArray2(arr)); // true
console.log(isArray3(arr)); // true
console.log(isArray4(arr)); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7.4 数组去重方法
JS 内置对象法。 无法对引用类型去重,数字和字符串相同时执行错误去重。
function unique(arr) { const res = [], map = {}; for (let i = 0; i < arr.length; i++) { if (!map[arr[i]]) { res.push(arr[i]); map[arr[i]] = 1; } } return res; }
1
2
3
4
5
6
7
8
9
10利用
Set
。function unique(arr) { return Array.from(new Set(arr)); }
1
2
3利用
includes()
。 如果结果数组res
不存在元素item
,则加入item
。function unique(arr) { const res = []; for (const item of arr) { if (!res.includes(item)) { res.push(item); } } return res; }
1
2
3
4
5
6
7
8
9利用
indexOf()
。 同上。无法对NaN
去重,因为indexOf()
基于===
比较,而NaN !== NaN
,因此所有NaN
加入结果。function unique(arr) { const res = []; for (const item of arr) { if (res.indexOf(item) === -1) { res.push(item); } } return res; }
1
2
3
4
5
6
7
8
9利用
indexOf()
,但对原数组做判断。 判断元素在原数组的下标是否等于本身。同样无法对NaN
去重,但不会有NaN
加入结果。function unique(arr) { const res = []; for (let i = 0; i < arr.length; i++) { if (arr.indexOf(arr[i]) === i) { res.push(arr[i]); } } return res; }
1
2
3
4
5
6
7
8
9排序后遍历。 排序后相同的元素将被排到一起。修改了原数组,新数组未按原数组顺序,无法对
NaN
去重,且数字和字符串相同时可能无法去重。function unique(arr) { arr.sort(); const res = [arr[0]]; for (let i = 1; i < arr.length; i++) { if (arr[i] !== arr[i - 1]) { res.push(arr[i]); } } return res; }
1
2
3
4
5
6
7
8
9
10利用
filter()
和indexOf()
。 方法 5 的简写版。function unique(arr) { return arr.filter((item, i) => arr.indexOf(item) === i); }
1
2
3
# 7.5 手写 flat
flat
是数组新的方法,它可以使一个数组扁平化,也就是数组嵌套数组时,将内层数组拆开,它的使用方法如下:
flat()
接收一个参数depth
,表示拍平层数,当depth = Infinity
时flat
将数组直接拍平为一维数组,当depth
省略时默认只拍平一层flat()
会自动忽略数组中的空位
利用 reduce
实现自己的 flatten
函数:
const flatten = (arr, depth) => {
if (depth === 0) return arr;
return arr.reduce((acc, val) => {
if (Array.isArray(val)) {
return acc.concat(flatten(val, depth - 1));
}
return acc.concat(val);
}, []);
}
2
3
4
5
6
7
8
9
利用扩展运算符直接拍平为一维数组:
function flatten(arr) {
const res = [];
for (const item of arr) {
if (Array.isArray(item)) {
res.push(...flatten(item));
} else {
res.push(item);
}
}
return res;
}
2
3
4
5
6
7
8
9
10
11
# 8 对象
# 8.1 对象的原型方法与静态方法
对象的原型方法:
hasOwnProperty()
:判断对象是否存在该自有属性。isPrototypeOf()
:判断对象是否是另一对象的原型(即obj.__proto__
)。propertyIsEnumerable()
:判断对象的该属性是否是可枚举的。toLocaleString()
、toString()
、valueOf()
,返回字符串(未重写时为"[object Object]"
)或对象本身。
对象的静态方法:
Object.is()
:判断两个参数是否相同,一般情况下等同于===
,但认为NaN
相等,+0
和-0
不等。console.log(NaN === NaN); // false console.log(Object.is(NaN, NaN)); // true console.log(+0 === -0); // true console.log(Object.is(+0, -0)); // false
1
2
3
4Object.create()
:返回一个新对象,以第一个参数作为新对象的原型,第二个参数用于添加属性。与new
的区别是,它不会调用构造函数。function Person () { this.age = 30; } const person = Object.create(Person.prototype, { name: { value: 'John', enumerable: true } }); console.log(person); // Person { name: 'John' }
1
2
3
4
5Object.assign()
:将第二个参数开始的对象通过浅拷贝合并到第一个对象上,并返回第一个对象的引用。同名属性将按参数顺序被覆盖。const obj1 = { a: 1, b: 2, c: 3 }; const obj2 = { d: 4, b: 5 }; const obj3 = { b: 6, e: 7 }; const obj = Object.assign(obj1, obj2, obj3) console.log(obj); // { a: 1, b: 6, c: 3, d: 4, e: 7 } console.log(obj === obj1); // true
1
2
3
4
5
6Object.keys()
、Object.values()
、Object.entries()
,接收一个对象,返回对应数组。- 返回对象自有的、可枚举的、非
Symbol
的属性 - 迭代顺序是不确定的,取决于 JS 引擎,因浏览器而异
- 注意与
Array.prototype.keys()
等的区别,数组是原型方法、返回迭代器,对象是静态方法、返回数组
- 返回对象自有的、可枚举的、非
Object.fromEntries()
:接收一个可迭代参数,返回该可迭代参数转变的对象。可迭代参数的每个元素应为一个entry
对象,例如二维数组、包含数组的Set
、Map
、包含数组的生成器对象。const arr = [['a', 1], ['b', 2], ['c', 3]]; const set = new Set([['a', 1], ['b', 2], ['c', 3]]); const map = new Map([['a', 1], ['b', 2], ['c', 3]]); function* gen() { for (let i = 0; i < 3; i++) { yield [i, i + 1]; } } console.log(Object.fromEntries(arr)); // { a: 1, b: 2, c: 3 } console.log(Object.fromEntries(set)); // { a: 1, b: 2, c: 3 } console.log(Object.fromEntries(map)); // { a: 1, b: 2, c: 3 } console.log(Object.fromEntries(gen())); // { '0': 1, '1': 2, '2': 3 } const obj = { a: 1, b: 2, c: 3 }; console.log(Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, v * 2]))); // { a: 2, b: 4, c: 6 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
:返回对象自有的、无论可否枚举的属性数组迭代顺序是:先按升序枚举数值键,再以插入顺序(或对象字面量定义顺序)枚举字符串键和符号键
如何只获取对象自有的可枚举属性?通过
for-in
加hasOwnProperty()
组合实现如何只获取对象自有的不可枚举属性?通过
Object.getOwnPropertyNames
对Object.keys()
进行filter
实现,如下:const obj = { a: 1, b: 2, c: 3 }; Object.defineProperty(obj, 'd', { value: 4, enumerable: false }); console.log(Object.getOwnPropertyNames(obj).filter(key => !Object.keys(obj).includes(key))); // ['d']
1
2
3
4
5
6
Object.getOwnPropertyDescriptor()
、Object.getOwnPropertyDescriptors()
:返回对象某个或全体属性的描述符,包括value
、writable
、enumerable
、configurable
,如果没有该属性则返回undefined
。const obj = { a: 1, b: 2, c: 3 }; console.log(Object.getOwnPropertyDescriptor(obj, 'a')); // { value: 1, writable: true, enumerable: true, configurable: true }
1
2Object.getPrototypeOf()
:返回对象的原型,等同于obj.__proto__
。Object.defineProperty()
、Object.defineProperties()
:定义对象属性,依次传入对象、属性名(定义单个的情况下)、描述符,返回原对象。- 也可以用来修改已有的属性
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.defineProperty(obj, 'd', { value: 4, enumerable: false })); // { a: 1, b: 2, c: 3 } console.log(Object.defineProperties(obj, { e: { value: 5, enumerable: false }, f: { value: 6, enumerable: true } })); // { a: 1, b: 2, c: 3, f: 6 } console.log(Object.keys(obj)); // [ 'a', 'b', 'c', 'f' ] console.log(Object.getOwnPropertyNames(obj)); // [ 'a', 'b', 'c', 'd', 'e', 'f' ]
1
2
3
4
5Object.freeze()
、Object.isFrozen()
:前者使一个对象被冻结并返回原对象,后者判断对象是否被冻结- 冻结后的对象无法添加、删除、修改属性,无法进行配置,无法修改对象原型
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.freeze(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isFrozen(obj)); // true obj.a = 2; console.log(obj.a); // 1
1
2
3
4
5Object.seal()
、Object.isSealed()
:前者使一个对象被密封并返回原对象,后者判断对象是否被密封- 密封后的对象无法添加、删除属性,无法进行配置,但可以修改现有属性
- 一个被冻结的对象同样是被密封的
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.seal(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isSealed(obj)); // true obj.d = 4; console.log(obj); // { a: 1, b: 2, c: 3 } console.log(Object.isFrozen(obj)); // false console.log(Object.freeze(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isFrozen(obj)); // true console.log(Object.isSealed(obj)); // true
1
2
3
4
5
6
7
8
9
10Object.preventExtensions()
、Object.isExtensible()
:前者使一个对象阻止扩展并返回原对象,后者判断对象是否可以扩展- 阻止扩展的对象无法添加属性,但可以删除和修改现有属性
- 一个被冻结或被密封的对象同样是不可扩展的
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.isExtensible(obj)); // true console.log(Object.preventExtensions(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isExtensible(obj)); // false obj.d = 4; console.log(obj); // { a: 1, b: 2, c: 3 } delete obj.b; console.log(obj); // { a: 1, c: 3 }
1
2
3
4
5
6
7
8
# 8.2 监听对象属性变化
通过
Object.defineProperty()
和setter
进行监听,Vue2.0 就是用这种方法:<ul></ul> <button onclick="update()">修改</button>
1
2const ul = document.querySelector('ul'); const person = { name: "John", sex: "男", age: 30 }; function render() { const html = ` <li>姓名:${person.name}</li> <li>性别:${person.sex}</li> <li>年龄:${person.age}</li>`; ul.innerHTML = html; } render(); Object.keys(person).forEach(key => { let value = person[key]; Object.defineProperty(person, key, { set(val) { value = val; // 如果使用this[key] = val,会导致调用栈溢出 render(); }, get() { // get方法必须存在 return value; } }); }); function update() { person.name = "Jane"; person.sex = "女"; person.age = 20; }
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缺点:一次性递归到底开销很大,如果数据很大,大量的递归导致调用栈溢出;不能监听对象的新增属性和删除属性;无法正确的监听数组的方法,当监听的下标对应的数据发生改变时。
通过 ES6 的
Proxy
实现:// 其他代码同上 const proxy = new Proxy(person, { // 这里不加get也可以 set(target, key, value) { target[key] = value; render(); } }); function update() { proxy.name = "Jane"; // 必须通过proxy修改 proxy.sex = "女"; proxy.age = 20; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 9 Set
、Map
# 9.1 Set
、WeakSet
、Map
、WeakMap
间的区别
WeakSet
和 WeakMap
:
- 键值必须为对象
- 键值为弱引用,无变量引用时对象被回收
- 不可枚举、不可遍历
- 无法获取数量(无
size
属性)
# 10 this
指向
汇总: this
始终指向一个对象。
- 普通函数调用,指向全局对象
- 对象方法调用,谁调用指向谁,只看调用不看引用
- 构造函数调用,指向新构造的实例,尽管可能不被构造函数返回
- 遇
apply
、call
、bind
,改变指向 - 匿名函数调用,一般指向全局对象
- 箭头函数调用,指向箭头函数定义域的
this
注意:在 node 下,全局 this
是一个空对象 {}
,注意它与全局对象 global
的区别。
# 10.1 普通函数调用
this
指向全局对象 window
,其中 var
定义的全局变量视为全局对象的属性。
let user = "let";
var age = 15;
window.pwd = 123;
function fn() {
console.log(this); // window对象
console.log(this.user); // undefined
console.log(this.age); // 15
console.log(this.pwd) // 123
}
fn();
2
3
4
5
6
7
8
9
10
# 10.2 对象方法调用
谁调用,指向谁,如果函数变量被赋值给其他对象,依然是谁调用指向谁,而不考虑函数来源(只看调用,不看引用)。
var name = "me";
var age = "123";
let obj = {
age: 15,
fn() {
console.log(this.name);
console.log(this.age);
}
}
let obj2 = {
name: "obj2",
age: 20
}
Object.prototype.proFn = function () {
console.log(this.age);
}
obj.fn(); // 输出 undefined 15,obj 调用,指向 obj
obj2.fn = obj.fn;
obj2.fn(); // 输出 obj2 20,obj2 调用,指向 obj2,即使该函数是由 obj.fn 赋值
obj.proFn(); // 输出 15,obj 调用原型,指向 obj
obj2.proFn(); // 输出 20,obj2 调用原型,指向 obj2
let fn1 = obj.fn, fn2 = obj.proFn;
fn1(); // 输出 me 123,window 调用,指向 window
fn2(); // 输出 123,window 调用,指向 window
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
如果出现多层调用,则 this
指向最近的上一层对象。
let obj = {
age: 15,
inner: {
age: 20,
fn() {
console.log(this.age);
}
}
}
obj.inner.fn(); // 20
2
3
4
5
6
7
8
9
10
# 10.3 构造函数调用
一般情况下,this
指向构造函数 new
的对象。如果构造函数当作普通函数使用,this
指向全局对象。
function A() {
this.name = "me";
var age = 15;
console.log(this.name); // me
console.log(this.age); // undefined
}
let a = new A();
console.log(a); // A { name: 'me' }
console.log(a.name); // me
console.log(a.age); // undefined
2
3
4
5
6
7
8
9
10
11
特殊情况下,如果构造函数返回一个对象(包括空对象),则 new
的对象不再是构造函数的实例,而是返回的对象。
构造函数执行过程中,this
依然试图指向构造函数的实例,虽然这个实例无法返回。
function A() {
this.name = "me";
console.log(this.name); // me
console.log(this.age); // undefined
return {
age: 20
};
}
let a = new A();
console.log(a); // { age: 20 }
console.log(a.name); // undefined
console.log(a.age); // 20
2
3
4
5
6
7
8
9
10
11
12
13
以上情况对返回值为基本类型、undefined
、null
时失效。
function A() {
this.name = "me";
console.log(this.name); // me
console.log(this.age); // undefined
return null;
}
let a = new A();
console.log(a); // A { name: 'me' }
console.log(a.name); // me
console.log(a.age); // undefined
2
3
4
5
6
7
8
9
10
11
# 10.4 apply
、call
、bind
调用
三者均可改变 this
的指向给第一个参数。区别在于:
apply
的第二个参数为可迭代对象call
的参数不固定bind
除返回一个函数外,与call
并无不同,它需要调用才可以执行,其他二者直接执行函数
注意下面示例中 12、13 行的区别。
let obj = {
age: 15,
fn(x, y) {
console.log(this.age + " " + x + " " + y);
}
}
let obj2 = {
age: 20
}
obj.fn(1, 2); // 15 1 2
obj.fn.apply(obj2, [3, 4]); // 20 3 4
obj.fn.call(obj2, 5, 6); // 20 5 6
obj.fn.call(obj2, [5, 6]); // 20 5,6 undefined
obj.fn.bind(obj2, 7, 8)(); // 20 7 8
2
3
4
5
6
7
8
9
10
11
12
13
14
# 10.5 匿名函数调用
匿名函数不属于任何对象,它的 this
一般指向全局对象。
let obj = {
fn() {
return function () {
console.log(this);
}
},
inner: {
fn() {
return function () {
console.log(this);
}
}
}
}
obj.fn()(); // window
obj.inner.fn()(); // window
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
定时器回调函数也是如此,例如:
let obj = {
fn() {
return function () {
setTimeout(function () {
console.log(this);
}, 1000);
}
}
}
obj.fn()(); // window
2
3
4
5
6
7
8
9
10
但注意,事件绑定时,this
指向事件源。(此时不是严格的匿名函数)
const btn = document.getElementById("btn");
btn.onclick = function () {
console.log(this); // 指向 btn
}
2
3
4
const btn = document.getElementById("btn");
btn.addEventListener("click", function () {
console.log(this); // 指向 btn
});
2
3
4
# 10.6 箭头函数
箭头函数没有自己的 this
,它会寻找定义箭头函数的作用域指向的 this
。
setTimeout(() => console.log(this), 1000); // window
let obj = {
fn() {
return function () {
setTimeout(() => console.log(this), 1000);
}
}
}
obj.fn()(); // window
2
3
4
5
6
7
8
let obj = {
fn() {
return () => setTimeout(() => console.log(this), 1000);
}
}
obj.fn()(); // obj
2
3
4
5
6
箭头函数作为对象属性值时,是对象定义域的 this
。
let obj = { fn: () => console.log(this) };
obj.fn(); // window
let obj2 = {
fn() {
return { fn: () => console.log(this) };
}
};
obj2.fn().fn(); // obj2,{ fn: [Function: fn] }
2
3
4
5
6
7
8
箭头函数不可被 apply
、call
、bind
改变 this
的指向。
let obj = { fn: () => console.log(this) };
let obj2 = { a: 123 };
obj.fn.call(obj2); // window
2
3
再看这个示例:
let obj = {
name: "me"
}
function fn() {
console.log(this);
return () => console.log(this);
}
fn()(); // 指向全局对象,输出两次 window
let fn2 = fn.call(obj); // 指向 obj,输出 obj 即 { name: 'me' }
fn2(); // 返回的箭头函数中的 this 指向上层的 this 即 obj,输出 obj 即 { name: 'me' }
2
3
4
5
6
7
8
9
10
# 11 apply
、call
、bind
apply
、call
和 bind
都可以改变函数内 this
的指向,不同之处在于:
apply
的第一个参数是this
指向的对象,第二个参数是一个可迭代对象,如Math.max.apply(null, [1, 2, 3, 4, 5])
call
的第一个参数是this
指向的对象,剩余参数数量不固定,如Math.max.apply(null, 1, 2, 3, 4, 5)
bind
与call
一致,但他返回一个改写了this
指向的函数,并不立即执行
# 11.1 手写 bind
Function.prototype.bind = function (context, ...args) {
var fn = this;
var noop = function () {};
var res = function (...rest) {
return fn.apply(this instanceof noop ? this : context, [...args, ...rest]);
}
if (this.prototype) {
noop.prototype = this.prototype;
}
res.prototype = new noop();
return res;
}
2
3
4
5
6
7
8
9
10
11
12
解释:
context
是要绑定的对象,...args
是bind
传入的不定参数。res
是返回的函数,如果只需要更改原函数fn
中this
的指向,这么写就可以了:Function.prototype.myBind = function (context, ...args) { var fn = this; // 原函数 var res = function (...rest) { return fn.apply(context, [...args, ...rest]); }; return res; }
1
2
3
4
5
6
7注意到原函数
fn
可能被当做构造函数使用,此时fn
中的this
应指向构造实例,与bind
绑定的对象无关。为了保证res
的实例也是一个fn
实例,我们有两种方法:- 让
res.prototype
指向this.prototype
,但这种方法会导致修改res
的原型方法时导致fn
的原型方法也被修改 - 让
res
通过原型链继承this
(即res.prototype = new this()
),但这种方法执行了fn
函数,会带来不必要的后果
Function.prototype.myBind = function (context, ...args) { var fn = this; // 原函数 var res = function (...rest) { // 这里的 this 与外层的 this 不同,如果 res 是普通函数,this 指向 window,如果 res 是构造函数,this 指向实例 return fn.apply(this instanceof res ? this : context, [...args, ...rest]); }; if (this.prototype) { // res.prototype = this.prototype; // res.prototype = new this(); } return res; }
1
2
3
4
5
6
7
8
9
10
11
12- 让
因此,我们可以构造一个
noop
函数,它是fn
构造函数的复制品,但是不做任何执行,让它绑定到fn
并让res
继承它:Function.prototype.myBind = function (context, ...args) { var fn = this; // 原函数 var noop = function () {}; // 空函数,仿造原构造函数,不破坏原型链且不做执行 var res = function (...rest) { // 这里的 this 与外层的 this 不同,如果 res 是普通函数,this 指向 window,如果 res 是构造函数,this 指向实例 return fn.apply(this instanceof res ? this : context, [...args, ...rest]); }; if (this.prototype) { noop.prototype = this.prototype; // 获得原函数的原型 } res.prototype = new noop(); // 继承原函数 return res; }
1
2
3
4
5
6
7
8
9
10
11
12
13
# 12 原型
prototype
:每个函数都有如此属性,其关联到另一个对象(称为原型对象)__proto__
:每个对象都有如此属性,指向其构造器函数的原型对象(父对象);函数也是对象,该属性指向Function
的原型对象(Function.prototype
)- 除了
a.__proto__
获取,还可以通过Object.getPrototypeOf(a)
获取 a.__proto__ === A.prototype
- 除了
constructor
:每个原型对象都有如此属性,指向构造器函数- 如果实例调用
constructor
属性,由于不存在的属性会从原型链寻找,因此也指向其构造函数 A.prototype.constructor === A
- 如果实例调用
细节问题:
a.__proto__ === A.prototype
说明什么?a
是构造函数A
的一个直接实例,或者说a
是通过new A()
构造的。a instanceof X
说明什么?a
是一个X
;a
是构造函数X
的一个直接或间接实例;构造函数X
的prototype
出现在a
的原型链上。a
的原型链是什么?a.__proto__
、a.__proto__.__proto__
、……一直到null
就是a
的原型链。A
的原型链是什么? 函数A
无论从何处继承,它的原型链只有:A -> Function.prototype -> Object.prototype -> null
。调用
a.fun()
,fun
定义在何处? 可能在:a
本身、构造函数A
的prototype
上、原型链上的某个X.prototype
上。什么对象没有原型?什么函数没有原型对象?
- 通过
Object.create(null)
创建的空对象没有原型,而{}
是有的(指向Object.prototype
) Object.prototype
的原型指向null
- 箭头函数无法作为构造函数使用,因此没有原型对象
console.log(Object.create(null).__proto__); // undefined console.log(Object.prototype.__proto__); // null console.log((() => {}).prototype); // undefined
1
2
3- 通过
# 12.1 new
一个对象的过程
以 var a = new A()
为例:
- 创建一个空对象:
var obj = {}
- 让构造器函数
A
的this
指向对象obj
,并执行A
中的函数体:var result = A.call(obj)
- 设置原型链,让对象
obj
的__proto__
属性指向构造器函数A
的原型对象:obj.__proto__ = A.prototype
- 判断构造器函数
A
的返回类型,如果是值类型(包括null
),结果返回obj
,如果是引用类型,返回引用类型的对象:a = result && typeof result === "object" ? result : obj
# 12.2 手写 new
函数
Function.prototype.new = function () {
var obj = {};
var result = this.call(obj);
obj.__proto__ = this.prototype;
return result && typeof result === "object" ? result : obj;
}
2
3
4
5
6
# 12.3 原型与 in
操作符、Object
方法
for-in
:遍历对象可枚举的非Symbol
属性,会遍历自有属性和原型属性in
:判断对象是否存在该属性,包括自有属性和原型属性Object.prototype.hasOwnProperty()
:判断对象是否存在该自有属性Object.keys()
:返回对象自有的、可枚举的、非Symbol
的属性名称数组Object.getOwnPropertyNames()
:返回对象自有的、无论可否枚举的、非Symbol
的属性名称数组Object.getOwnPropertySymbols()
:返回对象自有的、无论可否枚举的、Symbol
属性数组
function Person() {}
Person.prototype.name = "Carlo";
Person.prototype.age = 24;
const person = new Person();
person.job = "Developer";
Object.defineProperty(person, "country", { value: "China", enumerable: false });
const s = Symbol("s");
person[s] = "symbol";
for (const key in person) {
console.log(key); // job name age
}
console.log("job" in person); // true
console.log("name" in person); // true
console.log("country" in person); // true
console.log(s in person); // true
console.log(person.hasOwnProperty("job")); // true
console.log(person.hasOwnProperty("name")); // false
console.log(person.hasOwnProperty("country")); // true
console.log(person.hasOwnProperty(s)); // true
console.log(Object.keys(person)); // ["job"]
console.log(Object.getOwnPropertyNames(person)); // ["job", "country"]
console.log(Object.getOwnPropertySymbols(person)); // [Symbol(s)]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对象属性枚举顺序:
for-in
、Object.keys()
:不确定,取决于JS引擎,因浏览器而异Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Object.assign()
:先按升序枚举数值键,再以插入顺序(或对象字面量定义顺序)枚举字符串键和符号键
# 12.4 原型赋值问题
以下代码输出什么?
function Test() {}
Test.prototype.n = 0;
Test.prototype.inc = function() {
this.n += 1;
return this.n;
}
t1 = new Test();
t2 = new Test();
console.log(t1.inc()); // 1
console.log(t2.inc()); // 1
2
3
4
5
6
7
8
9
10
11
因为第 4 行的 this.n += 1
也就是 this.n = this.n + 1
。右边的 this.n
由于实例没有 n
属性会从原型中获取,赋值到左边的 this.n
,会给实例新增 n
属性,这时返回的都是实例的 n
属性。
以下代码输出什么?
function Test() {}
const test = new Test();
Test.prototype = {
name: "test"
};
console.log(test.name); // undefined
2
3
4
5
6
在 new
一个对象时,观察 11.1 提到的过程,可以发现实例的 __proto__
属性指向了构造函数的原型对象 Test.prototype
,称之为 A
。当让 Test.prototype
指向一个新对象(称之为 B
)时,test.__proto__
仍保持对 A
的指向,因此输出 undefined
。
注意与以下代码的区别:
function Test() {}
const test = new Test();
Test.prototype.name = "test";
console.log(test.name); // test
2
3
4
# 13 闭包
闭包(closure):内部函数可以访问外部函数的局部变量,即使外部函数被销毁。本质是 函数内部嵌套函数。
function fun1() {
var value = 0;
return function () {
value++;
console.log(value);
}
}
var fun = fun1();
fun(); // 1
2
3
4
5
6
7
8
9
闭包的作用:隐藏变量(通过访问局部变量的形式,间接访问变量),避免全局变量污染,设置私有变量等。
闭包的原理:作用域链,即当前作用域可以访问上级变量。注意,这里指的是函数声明时的作用域链,而非调用时的作用域链,参考闭包作为参数传递的情况。
参考:
- https://blog.csdn.net/Matildan/article/details/108349502 (opens new window)
- https://blog.csdn.net/dovlie/article/details/76339244 (opens new window)
- https://zhuanlan.zhihu.com/p/22486908 (opens new window)
# 13.1 闭包的应用
函数作为返回值:返回的函数调用时可以访问外部函数的局部变量,示例见上
闭包作为参数传递:函数作为参数传递时,访问的变量是该函数作用域下的变量,而非调用函数作用域下的变量
var num = 15; var fun1 = function (x) { console.log(x > num); } var fun2 = function (fun) { var num = 100; fun(30); } fun2(fun1); // true 访问的是 fun1 作用域的变量 num,即全局变量 num(15)
1
2
3
4
5
6
7
8
9定时器与闭包:分析以下几段代码的区别
for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000); } // 1 s 后,直接输出 3 3 3,因为先执行 for,再执行任务队列的 setTimeout for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 1000); }(i)); } // 1 s 后,直接输出 0 1 2,因为任务队列的 setTimeout 传递了参数 i for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 1000 * i); }(i)); } // 每隔 1 s 输出 0 1 2,因为同时启动了三个不同时间的定时器 for (let i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000); } // 1 s 后,直接输出 0 1 2,因为 let 每次循环中创建的是同名变量,不影响其他定时器访问 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对对象实现封装:将对象的属性改为函数的局部变量
const obj = (function () { var value = 0; return { add: function () { value++; }, getValue() { return value; } } })(); obj.add(); console.log(obj.getValue()); // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
# 13.2 闭包会造成内存泄漏吗?
大部分人认为,闭包的局部变量常驻内存,会增大内存使用量,并且使用不当很容易造成内存泄露。
内存泄漏
用不到(访问不到)的变量,依然占据内存空间,无法被再次利用。
但这取决于 JS 引擎。对于 Chrome 的 V8 引擎,它可以回收闭包中不使用的局部变量,保留使用的局部变量的引用。其他的浏览器引擎也有各自的处理方式。对于 IE8 的 JScript.dll 引擎,它会完整保留闭包的所有变量绑定,造成一定的内存消耗。
所以,我觉得说闭包一定能造成内存泄漏是不对的。对于闭包使用到的变量,我们需要使用它并占据空间;对于闭包不使用的变量,是否占据空间取决于 JS 引擎。
参考:https://www.cnblogs.com/rubylouvre/p/3345294.html (opens new window)
# 14 迭代器、生成器
迭代器是一个对象,通过不停地调用 next()
方法可以迭代得到多个值。
生成器是一个函数,通过 yield
关键字操作,生成一个生成器对象,可作为迭代器使用。
# 14.1 迭代器
迭代器是一个对象,实现了 Iterable
接口(next()
、return()
等方法)。
可迭代对象内置默认迭代器函数,并位于
Symbol.iterator
作为键的属性上,该函数执行后生成一个迭代器。字符串、数组、映射、集合、
arguments
对象、NodeList
等 DOM 集合类型都内置该工厂函数,也就是说它们是可迭代对象。得到一个可迭代对象的迭代器:
const str = "abc"; const f = str[Symbol.iterator]; console.log(f); // [Function: [Symbol.iterator]] console.log(f.call(str)); // Object [String Iterator] {}
1
2
3
4
迭代器可以进行以下操作:
for-of
循环、数组解构、扩展运算符、Array.from()
、Set
、Map()
、Promise.all()
、Promise.race()
、yield*
。- 如果上述操作的目标是可迭代对象,则使用它们的迭代器,例如
for(const ch of str)
和for (const ch of str[Symbol.iterator]())
是一致的。
- 如果上述操作的目标是可迭代对象,则使用它们的迭代器,例如
迭代器的
next()
方法返回一个对象,这个对象有value
和done
两个属性,当done
属性为false
时,上述操作暂停迭代。一个普通对象可以通过添加
Symbol.iterator
属性,属性值设置为一个函数,函数返回实现Iterator
接口的对象,这个对象就可以进行上述操作。const obj = { a: 1, b: 2, c: 3, [Symbol.iterator]() { let i = 0; const that = this; const keys = Object.keys(this); return { // 通过闭包来保存状态 next() { if (i < keys.length) { return { value: that[keys[i++]], done: false }; } else { return { value: undefined, done: true }; } } }; } }; for (let value of obj) { console.log(value); // 1 2 3 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23迭代器对象有一个可选的
return()
方法,迭代终止时会调用它,并“关闭”迭代器。如果迭代器没有关闭,可以从上次离开的地方继续迭代。但主动调用它不会使迭代器关闭,因此给不可关闭的迭代器(如数组迭代器)添加return()
方法不能使数组迭代器变成可关闭的。const arr = [1, 2, 3, 4, 5]; let it = arr[Symbol.iterator](); it.return = function() { console.log('return here'); return { value: undefined, done: true }; } let [ a, b, c ] = it; // return here console.log(a, b, c); // 1 2 3 let [ d, e ] = it; // return here console.log(d, e); // 4 5
1
2
3
4
5
6
7
8
9
10
# 14.2 生成器
生成器是一个函数。
function* () {}
是一个生成器函数,它的返回值是一个处于暂停状态的生成器对象,类似于迭代器,生成器对象实现了Iterable
接口并自引用。yield
关键字暂停生成器的执行,并将yield
表达式的值包装成{ value, done }
的形式传递给生成器对象;生成器对象通过next()
恢复生成器函数的执行并取得下一个yield
值。如果生成器函数有
return
,则return
作为最后一个迭代值。yield
表达式的返回值是生成器对象调用next()
传入的值(第一个next()
的参数会被忽略,因为要执行函数)。function* generator(val) { console.log(val); const newVal = yield 1; console.log(newVal); yield newVal; return 3; } const gen = generator("a"); // a,val 的值 console.log(gen.next("b")); // { value: 1, done: false },参数 "b" 被忽略 console.log(gen.next("c")); // { value: 'c', done: false },参数 "c" 被 yield 1 返回并赋值给 newVal console.log(gen.next("d")); // { value: 2, done: true },返回值 2 console.log(gen.next("e")); // { value: undefined, done: true },迭代已结束
1
2
3
4
5
6
7
8
9
10
11
12
13yield*
表达式可以将后面的一个可迭代对象依次进行yield
。function* generator() { yield [1, 2, 3]; yield* [4, 5, 6]; } const gen = generator(); console.log(gen.next()); // { value: [1, 2, 3], done: false } console.log(gen.next()); // { value: 4, done: false } console.log(gen.next()); // { value: 5, done: false } console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: undefined, done: true }
1
2
3
4
5
6
7
8
9
10
11yield*
表达式后可以跟生成器本身,因此可以实现递归:function* nTimes(n) { if (n > 0) { yield* nTimes(n - 1); yield n; } } for (let x of nTimes(3)) { console.log(x); // 1 2 3 }
1
2
3
4
5
6
7
8
9
10生成器对象调用
return()
方法会使生成器强行关闭,并使用return()
的参数作为最终的value
,done
为true
;生成器关闭后不可恢复。生成器对象调用
throw()
方法会使生成器暂停并抛出错误,如果错误未被处理,则生成器被关闭;如果生成器函数内部处理了错误,则生成器不会关闭且可恢复执行,但会跳过一次yield
。
# 15 Promise
与异步
Promise
是异步编程的一种解决方案,解决地狱回调的问题。
Promise
的作用:Promise
是异步微任务,解决了异步多层嵌套回调的问题,让代码的可读性更高,更容易维护。Promise
的使用:Promise
是 ES6 提供的一个构造函数,可以使用Promise
构造函数new
一个实例,Promise
构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数resolve
和reject
,resolve
将Promise
的状态由等待变为成功,将异步操作的结果作为参数传递过去;reject
则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。实例创建完成后,可以使用then
方法分别指定成功或失败的回调函数,也可以使用catch
捕获失败,then
和catch
最终返回的也是一个Promise
,所以可以链式调用。Promise
的特点:- 对象的状态不受外界影响(
Promise
对象代表一个异步操作)。有三种状态:pending
(执行中)、resolved
(成功,又称fulfilled
)、rejected
(拒绝) ,其中pending
为初始状态,fulfilled
和rejected
为结束状态(结束状态表示Promise
的生命周期已结束)。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能(状态凝固了,就不会再变了,会一直保持这个结果):从pending
变为resolved
、从pending
变为rejected
。 resolve
方法的参数是then
中回调函数的参数,reject
方法中的参数是catch
中的参数。then
方法和catch
方法只要不报错,返回的都是一个fullfilled
状态的Promise
,解决值为回调函数的返回值。
- 对象的状态不受外界影响(
Promise
的静态方法:Promise.resolve()
:返回的Promise
对象状态为fulfilled
,并且将该value
传递给对应的then
方法。我们认为Promise.resolve()
是普通Promise
对象调用resolve()
的语法糖,即:Promise.resolve(3); // 等同于 new Promise((resolve, reject) => resolve(3));
1
2
3但如果传入的是一个
Promise
对象,那么Promise.resolve()
将不做任何修改、原封不动地返回这个实例。Promise.reject()
:返回一个状态为rejected
的Promise
对象,并将给定的失败信息传递给对应的处理方法。Promise.all()
:返回一个新的Promise
对象,该Promise
对象在参数对象里所有的Promise
对象都成功的时候才会触发成功,解决值为所有解决值的数组;一旦有任何一个iterable
里面的Promise
对象失败则立即触发该Promise
对象的失败,失败值为该失败值。const one = Promise.resolve("one"); const two = Promise.resolve("two"); const three = Promise.reject("three"); Promise.all([one, two]).then(console.log); // [ 'one', 'two' ] Promise.all([one, three]).catch(console.log); // three
1
2
3
4
5Promise.race()
:当参数里的任意一个子Promise
被成功或失败后,父Promise
马上也会用子Promise
的成功返回值或失败详情作为参数调用父Promise
绑定的相应句柄,并返回该Promise
对象。注意子Promise
的落定顺序可能被setTimeout
改变。const one = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'one'); }); const two = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'two'); }); Promise.race([one, two]).then(console.log); // two
1
2
3
4
5
6
7
# 15.1 前端怎么实现异步
JS 是单线程,程序运行是同步的。但如果有的代码长时间执行会影响后面代码的运行,因此需要异步编程。
基本原理是将异步代码加入任务队列,当当前调用栈清空后再取出任务队列中的任务。
常用方法:
- 回调函数。
- 事件监听。
- 发布/订阅。
Promise
。- 生成器。
async/await
异步函数。
# 15.2 宏任务和微任务
宏任务(macro task)可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
微任务(micro task)可以理解是在当前宏任务执行结束后立即执行的任务。
- 宏任务:整体代码、
new Promise
内的代码、setTimeout
、setInterval
、I/O
事件、UI
交互事件等 - 微任务:
Promise.then
、process.nextTick()
如果微任务内有新的宏任务,新的宏任务会等当前微任务队列清空后再执行。
# 16 class
与继承
class
是 ES6 新增关键字,用于定义一个类,可通过 constructor()
初始化,可被 extends
关键字继承;实际上是构造函数的语法糖,基于组合寄生式继承。
# 16.1 JS 继承的几种方式
继承方式:原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、ES6 Class。
参考:
- https://blog.csdn.net/jatej/article/details/120317973 (opens new window)
- https://blog.csdn.net/m0_45857808/article/details/123866779 (opens new window)
原型链继承:将子类的原型对象设置为父类的一个实例。
- 原理:子类的实例可以使用原型链的所有属性和方法,包括父类构造函数的和父类原型的。
- 缺点:新实例无法向父类构造函数传参;继承单一;所有新实例都会共享父类实例的属性。
function Parent() { this.name = 'parent'; this.nums = [1, 2, 3]; } function Child() { this.type = 'child'; } Child.prototype = new Parent(); // 原型链继承 const c1 = new Child(); console.log(c1.name); // parent,来自原型,即父类实例 const c2 = new Child(); c1.nums.push(4); console.log(c2.nums); // [1, 2, 3, 4],nums来自原型,所以c1和c2共享nums
1
2
3
4
5
6
7
8
9
10
11
12
13
14借用构造函数继承:调用父类构造函数,但通过
call
把内部的this
指向子类实例。- 原理:父类构造函数的
this
指向子类实例,因此子类实例获得了父类属性和方法。子类可继承多个父类(call
多个),同时子类可向父类传参。 - 缺点:不能继承父类原型属性;无法实现构造函数复用;每个新实例都有父类构造函数的副本。
function Parent(name) { console.log('Parent constructor'); this.name = name; } Parent.prototype.say = function() { console.log('Parent say'); } function Child(name) { Parent.call(this, name); // 构造函数继承 this.type = 'child'; } const c1 = new Child("child1"); // Parent constructor console.log(c1.name); // child1 console.log(c1.say); // undefined const c2 = new Child("child2"); // Parent constructor,每次都要调用父类构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16- 原理:父类构造函数的
组合继承:组合使用原型链继承和构造函数继承。
- 原理:同时使用这两种继承方式,父类实例的属性和方法被直接继承到子类实例,父类原型的属性和方法通过原型链获取,这样不会引起共享属性问题。同时子类可以传参。
- 缺点:调用了两次父类构造函数。
function Parent(name) { console.log('Parent constructor'); this.name = name; } Parent.prototype.say = function() { console.log('Parent say'); } function Child(name) { Parent.call(this, name); this.type = 'child'; } Child.prototype = new Parent(); // Parent constructor const c = new Child("child"); // Parent constructor console.log(c.name); // child c.say(); // Parent say
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15原型式继承:使用一个函数,传入父类实例,函数体内定义子类,并进行原型链继承,最后返回子类实例。等同于
Object.create()
方法。- 原理:与原型链继承方法类似,只是进行了一次封装,没有显式定义子类构造函数。
- 缺点:与原型链继承方法类似,所有新实例都会共享父类实例的属性;无法实现复用。
function Parent(name) { this.name = name; } const parent = new Parent('parent'); function createObj(parent) { function Child() {} Child.prototype = parent; return new Child(); } const child = createObj(parent); // 等同于 Object.create(parent) console.log(child.name); // parent
1
2
3
4
5
6
7
8
9
10
11
12
13寄生式继承:基于原型式继承,但外加一层函数封装,为子类实例添加属性或方法。
- 原理:与原型式继承类似。
- 缺点:与原型式继承类似。
function Parent(name) { this.name = name; } const parent = new Parent('parent'); function clone(obj) { const child = Object.create(obj); child.getName = function() { return this.name; } return child; } const child = clone(parent); console.log(child.name); // parent console.log(child.getName()); // parent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16组合寄生式继承:结合组合式继承和寄生式继承,使用一个空构造函数仿造父类构造函数。是相对最优的继承方式。
- 原理:组合式继承的构造函数继承仍不变,然后使用一个空函数,将其原型对象赋值为父类的原型对象以实现仿造,再让子类通过原型链继承继承空函数(执行空函数无副作用),然后修改子类原型对象的
constructor
指向。这样子类继承了父类的所有私有属性、方法并可使用父类原型的属性、方法。 - 缺点:无。
function Parent(name) { this.name = name; this.nums = [1, 2, 3]; } Parent.prototype.say = function() { console.log("Parent say"); } function Child(name, age) { Parent.call(this, name); this.age = age; } const F = function() {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; const child = new Child("child", 10); console.log(child); // { name: 'child', nums: [ 1, 2, 3 ], age: 10 } console.log(child.name); // child child.say(); // Parent say child.nums.push(4); // child.nums 为 [ 1, 2, 3, 4 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23由于第 14-16 行可以抽象为原型式继承,因此可以将组合寄生式继承(即第 14-17 行)整理为函数:
function extend(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; }
1
2
3
4- 原理:组合式继承的构造函数继承仍不变,然后使用一个空函数,将其原型对象赋值为父类的原型对象以实现仿造,再让子类通过原型链继承继承空函数(执行空函数无副作用),然后修改子类原型对象的
# 17 内存回收
浏览器的 JavaScript 具有自动垃圾回收机制(GC: Garbage Collection),也就是说,执行环境会负责管理代码执行过程中使用的内存。
其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
# 17.1 内存回收的两个方法
- 引用计数(reference counting):将对象被其他对象引用的情况计数,如果没有其他对象指向该对象(零引用),对象被 GC 回收。但无法处理循环引用。
- 标记清除(mask sweep):判断对象是否可以获得,从根开始找所有根引用的对象,然后找对象引用的对象,以此类推,直到最终无法找到的对象被清除。
IE6,7 使用引用计数方式对 DOM 对象进行垃圾回收,从 2012 年起,所有现代浏览器都使用了标记清除垃圾回收算法。
不可获得的对象 = 有零引用的对象(1) + 循环引用的无用对象(2) + 无法从根对象查询到的对象(3)
很显然,标记清除算法可以对(2)进行很好地清理,但是会误清理掉(3),但是实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。
WeakMap
里面对键值的引用就是弱引用,不会被计入垃圾回收机制,如果键值对象被回收,那么 WeakMap
对应的键值对会自动消失。
# 17.2 内存泄漏的情况
- 意外的全局变量
- 闭包(参考闭包一节)
- 未被清空的定时器
- 未被销毁的事件监听
- DOM 引用
# 18 防抖和节流
防抖和节流用来处理 响应跟不上触发频率 的情况,如鼠标移动事件、滚动条滚动事件等。
两者的区别: 防抖的周期随函数触发而后移,节流的周期不随函数触发而后移。防抖适合处理高频但停顿的事件,节流适合处理高频且连续的事件。
参考:
- https://www.cnblogs.com/wjgoblin/p/10950886.html (opens new window)
- https://blog.csdn.net/hupian1989/article/details/80920324 (opens new window)
# 18.1 防抖
防抖: 将几次操作合并为一次操作进行,只有最后一次操作 被触发。
示例:用户 resize
调整窗口大小,会发生多次 resize
事件,利用防抖使用户最终停下来后再触发事件;用户在搜索框打字,当用户停下来后触发关键词联想事件。
实现过程:防抖分延迟防抖(先周期后触发)和前缘防抖(先触发后周期),下面以延迟防抖为例。设定定时器,延时触发函数,但若延迟时间内函数再次被触发,取消之前的计时器重新计时,如此,只有最后一次操作被触发。
function debounce(fn, delay) {
let timer = null;
return function () {
const that = this, args = arguments;
timer && (clearTimeout(timer), timer = null);
timer = setTimeout(() => fn.apply(that, args), delay);
}
}
2
3
4
5
6
7
8
这里的
fn.apply
起何作用?为了把调用节流函数返回的匿名函数的
this
和arguments
传给fn
使用,绑定事件后,这里this
为全局对象window
(通常不用),arguments
包含事件。可以有别的写法吗?
可以使用 ES6 的扩展运算符,写成
setTimeout(() => fn(...args), delay)
,本质上,就是调用fn
函数并传不确定的参数。
# 18.2 节流
节流: 一段时间内只触发一次函数,若周期内有新触发,不执行,除非周期结束且开始触发新周期。
示例:用户 resize
调整窗口大小,会发生多次 resize
事件,利用节流使用户在调整过程中以稳定的节奏触发事件。
实现过程:节流分延迟节流(先周期后触发)和前缘节流(先触发后周期),下面以延迟节流为例。设定定时器,延时触发函数,但若延迟时间内函数再次被触发,不予操作且不予重新计时,直到函数执行后再清除计时器。
function throttling(fn, delay) {
let timer = null;
return function () {
const that = this, args = arguments;
!timer && (timer = setTimeout(() => {
fn.apply(that, args);
clearTimeout(timer);
timer = null;
}, delay));
}
}
2
3
4
5
6
7
8
9
10
11
比较两个代码,就很好理解,一个是停顿处理,一个是平滑处理。
# 19 深拷贝和浅拷贝
- 浅拷贝(赋值): 与原数据指向相同,无论如何改变新数据,原数据都将被改变
- 深拷贝: 与原数据指向不同,无论如何改变新数据,都不会使原数据改变
# 19.1 JSON转换
JSON.stringify
将 JavaScript 对象转换为字符串,JSON.parse
将 JavaScript 字符串转换为对象,据此进行深拷贝。
function deepCopy(source) {
if (!isObject(source)) {
return source;
}
return JSON.parse(JSON.stringify(source));
}
function isObject(obj) {
return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
缺点:
- 会破坏部分数据:
- 时间对象会被复制为字符串,而非时间对象
RegExp
对象、Error
对象会被复制为空对象{}
NaN
、Infinity
、-Infinity
会被复制为null
- 函数和
undefined
会直接丢失
- 会舍弃原对象的原型链,直接继承自
Object.prototype
或Array.prototype
- 不能处理循环引用的情况
- 当数据层次很深时,会栈溢出
# 19.2 普通递归
讨论原数据的类型,可能为:值类型(包括 null
)、对象、数组,然后递归处理子层数据。
function deepCopy(source) {
if (!isObject(source)) {
return source;
}
const target = Array.isArray(source) ? [] : {};
for (let key in source) {
if (source.hasOwnProperty(key)) {
target[key] = deepCopy(source[key]);
}
}
return target;
}
function isObject(obj) {
return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
缺点:
- 同样会舍弃原对象的原型链,两个解决方案:
- 返回结果前加上
target.__proto__ = source.__proto__;
- 第 5 行改为
const target = new source.constructor;
- 返回结果前加上
- 同样不能处理循环引用的情况
- 当数据层次很深时,会栈溢出
# 19.3 循环引用优化
用 Map
记录对象中的所有对象,并记录对象的引用关系,即每个出现过的对象,有一个之前创建过的新对象与之对应,拿出新对象返回。
const map = new Map();
function deepCopy(source) {
if (!isObject(source)) {
return source;
}
if (map.has(source)) {
return map.get(source);
} else {
const target = new source.constructor;
map.set(source, target);
for (let key in source) {
if (source.hasOwnProperty(key)) {
target[key] = deepCopy(source[key]);
}
}
return target;
}
}
function isObject(obj) {
return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
缺点:
- 当数据层次很深时,会栈溢出
- 对正则等特殊对象不能较好地处理
# 19.4 jQuery.extend()
函数
第一个参数表示是否深拷贝,第二个参数为目标对象,第三个参数为原对象。
function deepCopy(source) {
if (!isObject(source)) {
return source;
}
const target = new source.constructor;
$.extend(true, target, source);
return target;
}
function isObject(obj) {
return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
11
12
# 20 跨域请求与同源策略
不同源的 ajax 请求,会出现跨域问题。
同源策略:同源指 协议、域名、端口 相同,子域名也不同源。一段脚本只能读取同源信息。假如没有同源策略,黑客可以在他页面利用 iframe
嵌入某个登录界面,并用 JS 读取 input
内容、cookies 等。
同源策略会导致这些限制:cookie、LocalStorage
、IndexDB
无法读取;DOM 无法获得、ajax 请求不能发送。
跨域发生的时机:请求发送到后端,后端返回数据,浏览器接收数据时被跨域报错拦截。而非发出请求时。
# 20.1 解决跨域的方法
有 jsonp 跨域、document.domain
、服务器代理、window.name
、window.postMessage
、WebSocket
、CORS 等方法。
jsonp 跨域。jsonp 只能解决
GET
请求。script
的src
、link
的href
、img
的src
不受跨域限制。通过<script>
的src
属性,服务器动态生成JS脚本,通过调用本地回调函数返回数据。<script> function fn(rs) { console.log(rs); } </script> <html> <script src="http://localhost:3000/get"></script> </html>
1
2
3
4
5
6
7
8const express = require('express'); const app = express(); app.get('/get', (req, res) => { const data = JSON.stringify({a: 1, b: 2}); // 传递数据 const fnStr = `fn(${data})`; // 函数调用语句 res.end(fnStr); }); app.listen(3000, () => { console.log('通过 3000 端口访问'); });
1
2
3
4
5
6
7
8
9
10也可以用 jQuery 直接把 ajax 封装为 jsonp:
$.ajax({ type: 'GET', url: 'http://localhost:3000/get', success: function (result) { console.log(result); }, dataType: 'jsonp' // 必须指定 });
1
2
3
4
5
6
7
8const express = require('express'); const app = express(); app.get('/get', (req, res) => { const data = JSON.stringify({a: 1, b: 2}); // 传递数据 // res.json(data); res.jsonp(data); }); app.listen(3000, () => { console.log('通过 3000 端口访问'); });
1
2
3
4
5
6
7
8
9
10document.domain
。通过给document.domain
赋值可以设置当前域为 当前域的父域,即可访问 cookie 和 DOM。console.log(document.domain); // a.test.com document.domain = 'test.com';
1
2服务器代理。内部服务器代理请求跨域 URL,然后返回数据。静态浏览器和代理服务器同源,代理服务器再向后端服务器发请求,服务器之间不存在同源限制。如 Nginx、node 接口等。
window.name
。在同一个窗口里,window.name
可以在前一个网页设置,再后一个网页获取,即window.name = data
。window.postMessage
。该方法允许跨窗口通信,例如父窗口向子窗口发消息。var popup = window.open('http://bbb.com', 'title'); // 打开子窗口 popup.postMessage('Hello World!', 'http://bbb.com'); // 信息内容,接收消息的源(发向地)
1
2该方法还可以实现读写其他窗口的
localStorage
。**
WebSocket
。**如果WebSocket
的头信息的Origin
在白名单内,服务器就允许跨源通信。CORS。跨源资源分享(Cross-Origin Resource Sharing),允许任何类型请求。
简单请求:方法是
HEAD
、GET
、POST
之一,且头信息不超过Accept
等几个字段。否则为非简单请求。如果 ajax 是简单请求,浏览器为请求头添加
Origin
字段表示请求源,如果请求源不在许可范围内,浏览器的响应头不包含Access-Control-Allow-Origin
字段。如果在许可范围内,响应头会包含此字段,并且值为Origin
值。如果ajax是非简单请求,浏览器首先发送预检请求
OPTIONS
,如果服务器确认跨源请求,可以回应Access-Control-Allow-Origin
等字段,浏览器再发出非简单请求,与简单请求类似包含Origin
、响应包含Access-Control-Allow-Origin
。手动设置请求头可以实现跨域:
app.get('/get', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); // 允许任意源访问 // res.setHeader('Access-Control-Allow-Origin', 'http://www.baidu.com'); // 允许指定源访问 res.send(Date.now().toString()); });
1
2
3
4
5也可以使用 CORS 包。
# 21 Storage
和 cookie
# 21.1 cookie、localStorage
、sessionStorage
的区别
相同点:
- 存储位置:都是浏览器存储,都存储在浏览器本地。
- 数据共享:都遵循同源原则,
sessionStorage
还限制是同一页面。
不同点:
- 写入方式:cookie 是服务器端写入的,
localStorage
和sessionStorage
是前端写入的。 - 生命周期:cookie 的生命周期由服务器端在写入时确定,
localStorage
写入后一直存在(除非手动清除),sessionStorage
页面关闭时就被清除。 - 存储大小:cookie 存储空间小(大约 4 KB),
localStorage
和sessionStorage
存储空间大(大约5 MB)。 - 发送请求时是否携带:cookie 在前端给后端发送请求时会自动携带,
localStorage
和sessionStorage
不会。 - 路径限制:cookie 可以限制只属于某个路径下,
localStorage
和sessionStorage
不可以。 - 作用域:cookie 和
localStorage
在所有同源窗口共享,sessionStorage
不在不同的浏览器窗口共享,即使是同一页面。 - 应用场景:cookie 一般用于存储登录验证信息 Session ID 或 token,
localStorage
常存储不易变动的数据(减轻服务器压力),sessionStorage
检测用户是否刷新页面(如音乐播放器恢复进度条)。
# 21.2 常见问题
cookie 可以由前端写入吗? 可以,对
document.cookie
赋值。token 可以放在 cookie 中吗? 能。 token 一般是用来判断用户是否登录的,它内部包含的信息有:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)。token 可以存放在 cookie 中,token 是否过期,应该由后端来判断,不该前端来判断,所以 token 存储在 cookie 中只要不设置 cookie 的过期时间就 ok 了,如果 token 失效,就让后端在接口中返回固定的状态表示 token 失效,需要重新登录,再重新登录的时候,重新设置 cookie 中的 token 就行。
token 认证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端签发一个 token,并把它发送给客户端
- 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
- 服务端收到请求后,需要验证请求里带有的 token,如验证成功则返回对应的数据
如何实现可过期的
localstorage
数据?localStorage
只能用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。所以要实现可过期的localStorage
缓存的中重点就是:如何清理过期的缓存。目前有两种方法,一种是惰性删除,另一种是定时删除。惰性删除是指,某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。
实现方法是,存储的数据类型是个对象,该对象有两个
key
,一个是要存储的value
值,另一个是当前时间。获取数据的时候,拿到存储的时间和当前时间做对比,如果超过过期时间就清除 cookie。定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对 CPU 的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对
localStorage
空间的浪费。实现方法:获取所有设置过期时间的
key
判断是否过期,过期就存储到数组中,遍历数组,每隔 1 s(固定时间)删除 5 个(固定个数),直到把数组中的key
从localstorage
中全部删除。
# 22 XSS 和 CSRF
XSS:跨站脚本攻击(Cross Site Scripting),向目标网站插入恶意代码,大量用户访问网站时运行恶意脚本获取信息。
CSRF:跨站点请求伪造(Cross Site Request Forgery),盗用用户身份发起请求。
# 22.1 XSS
XSS 是跨站脚本攻击(Cross Site Scripting),不写为 CSS 是为了避免和层叠样式表(Cascading Style Sheets)的缩写混淆,所以将跨站脚本攻击写为 XSS。
攻击者可以通过向 Web 页面里面插入 script 代码,当用户浏览这个页面时,就会运行被插入的 script 代码,达到攻击者的目的。
- XSS 的危害:一般是泄露用户的登录信息 cookie,攻击者可以通过 cookie 绕过登录步骤直接进入站点。
- 获取 cookie:网站中的登录一般都是用 cookie 作为某个用户的身份证明,这是服务器端返回的一串字符。如果 cookie 被攻击者拿到,那么就可以绕过密码登录。当空间、论坛如果可以被插入 script 代码,那么进入空间或者论坛的人的账号就可以轻易被攻击者获取。
- 恶意跳转:直接在页面中插入
window.location.href
进行跳转。
- XSS 的分类:分为反射型和存储型。
- 反射型 XSS(非持久型 XSS):临时通过 url 访问网站,网站服务端将恶意代码从 url 中取出,拼接在 HTML 中返回给浏览器,用户就会执行恶意代码。
- 存储型 XSS(持久型 XSS):将恶意代码以留言的形式保存在服务器数据库,任何访问网站的人都会受到攻击。
- 预防 XSS 攻击的方案:基本是对数据进行严格的输出编码,比如 HTML 元素的编码,JavaScript 编码,CSS 编码,url 编码等等。
- 浏览器的防御和“X-XSS-Protection”有关,默认值为 1,即默认打开 XSS 防御,可以防御反射型的 XSS,不过作用有限,只能防御注入到 HTML 的节点内容或属性的 XSS,例如 URL 参数中包含 script 标签。不建议只依赖此防御手段。
- 防御 HTML 节点内容,通过转义
<
为<
以及>
为>
来实现防御 HTML 节点内容。 - 预防 HTML 属性,通过转义
"
为&quto;
来实现防御,一般不转义空格,但是这要求属性必须带引号。 - 预防 JavaScript 代码,通过将数据进行 JSON 序列化。
- 防御富文本是比较复杂的工程,因为富文本可以包含 HTML 和 script,这些难以预测与防御,建议是通过白名单的方式来过滤允许的 HTML 标签和标签的属性来进行防御,大概的实现方式是:
- 将 HTML 代码段转成树级结构的数据
- 遍历树的每一个节点,过滤节点的类型和属性,或进行特殊处理
- 处理完成后,将树级结构转化成 HTML 代码
- 开启浏览器 XSS 防御:Http Only cookie,禁止 JavaScript 读取某些敏感 cookie,攻击者完成 XSS 注入后也无法窃取此 cookie
# 22.2 CSRF
CSRF 是跨站点请求伪造(Cross Site Request Forgery),和 XSS 攻击一样,有巨大的危害性,就是攻击者盗用了用户的身份,以用户的身份发送恶意请求,但是对服务器来说这个请求是合理的,这样就完成了攻击者的目标。
CSRF 攻击的过程原理:
- 用户打开浏览器,访问目标网站 A,输入用户名和密码请求登录。
- 用户信息在通过认证后,网站 A 产生一个 cookie 信息返回给浏览器,这个时候用户以可正常发送请求到网站 A。
- 用户在没有退出网站 A 之前在同一个浏览器打开了另一个新网站 B。
- 新网站 B 收到用户请求之后返回一些攻击代码,并发出一个请求要求访问返回 cookie 的网站 A。
- 浏览器收到这些攻击性代码之后根据新网站 B 的请求在用户不知道的情况下以用户的权限操作了 cookie 并向网站A服务器发起了合法的请求。
预防 CSRF 攻击的主要策略:
使用验证码,在表单中添加一个随机的数字或者字母验证码,强制要求用户和应用进行直接的交互。
HTTP 中 Referer 字段,检查是不是从正确的域名访问过来,它记录了 HTTP 请求的来源地址。
验证 HTTP Referer 字段的好处就是实施起来特别简单,普通的网站开发不需要特别担心 CSRF 漏洞,只需要在最后面设置一个拦截器来验证 referer 的值就可以了,不需要改变已有的代码逻辑,非常便捷。但是这个方法也不是万无一失的,虽然 referer 是浏览器提供的,但是不同的浏览器可能在 referer 的实现上或多或少有自身的漏洞,所以使用 referer 的安全保证是通过浏览器实现的。
使用 token 验证,在 HTTP 请求头中添加 token 字段,并且在服务器端建立一个拦截器验证这个 token,如果 token 不对,就拒绝这个请求。
使用 token 验证的方法要比 referer 更安全一些,需要把 token 放在一个 HTTP 自定义的请求头部中,解决了使用 get 或者 post 传参的不便性。
# 23 JS 事件流
事件流:从页面中接收事件的顺序,即事件在页面中传播的顺序。
# 23.1 事件捕获与事件冒泡
微软认为事件发生的顺序应该是事件冒泡(bubbling),即由内向外;网景认为事件发生的顺序应该是事件捕获(capturing),即由外向内。W3C 采取折中方式:先捕获后冒泡。
onclick
:绑定的onclick()
事件在冒泡阶段执行,例如:<body> <div onclick="divClick()"> <p onclick="pClick()"> <span onclick="spanClick()">点击事件</span> </p> </div> </body> <script> function divClick() { console.log('divClick'); } function pClick() { console.log('pClick'); } function spanClick() { console.log('spanClick'); } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18点击后,输出顺序为
spanClick
、pClick
、divClick
(冒泡顺序)。stopPropagation()
:要阻止事件冒泡,可以使用stopPropagation()
函数:function pClick(event) { console.log('pClick'); event.stopPropagation(); }
1
2
3
4addEventListener
:接收三个参数,分别是event
(事件名)、function
、useCapture
。第三个参数默认为false
,如果设置为true
则表示事件在捕获阶段执行,例如:<body> <div id="div"> <span id="span">点击事件</span> </div> </body> <script> const div = document.getElementById('div'); const span = document.getElementById('span'); div.addEventListener('click', function(e) { console.log('div bubbling'); }, false); span.addEventListener('click', function(e) { console.log('span bubbling'); }, false); div.addEventListener('click', function(e) { console.log('div capturing'); }, true); span.addEventListener('click', function(e) { console.log('span capturing'); }, true); </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21点击后,输出顺序为
div capturing
、span capturing
、span bubbling
、div bubbling
。
# 23.2 事件代理
由于事件 event
的 target
为最深层的元素,我们可以将一组元素的事件绑定到他的外层元素上,这样事件会在冒泡阶段被传递到外层函数上执行。好处是可以同时给父元素的多个子元素绑定事件,节省开销。
<ul id="list">
<li>red</li>
<li>yellow</li>
</ul>
</body>
<script>
const list = document.getElementById('list');
list.addEventListener('click', e => {
const li = e.target;
if (li.tagName.toLowerCase() === 'li') {
console.log("Color is " + li.textContent);
}
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
这样点击哪个 li
就输出对应的颜色。
# 23.3 IE兼容性
IE9以下不兼容 addEventListener
,可以使用 attachEvent(event, function)
函数。
它在冒泡阶段调用,并且事件名要加上 "on"
前缀。
# 24 Ajax
Ajax(Asynchronous Javascript And XML)是异步 JavaScript 和 XML。使用 Ajax 技术网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,这使得程序能够更快地回应用户的操作。
XML(Extensible Markup Language)是可扩展标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。
XHR(XMLHttpRequest)是 XML HTTP 请求,用于与服务器交互数据,是 Ajax 功能实现所依赖的对象。
# 24.1 Ajax的创建过程
创建XHR对象:
new XMLHttpRequest()
设置请求参数:
request.open(Method, 服务器接口地址);
发送请求:
request.send()
,如果是get
请求不需要参数,post
请求需要参数request.send(data)
及请求头监听请求成功后的状态变化:根据状态码进行相应的处理
XHR.onreadystatechange = function () { if (XHR.readyState === 4 && XHR.status === 200) { console.log(XHR.responseText); // 主动释放,JS 本身也会回收的 XHR = null; } };
1
2
3
4
5
readyState
值说明:
- 0:初始化,XHR 对象已经创建,还未执行
open
- 1:载入,已经调用
open
方法,但是还没发送请求 - 2:载入完成,请求已经发送完成
- 3:交互,可以接收到部分数据
- 4:数据全部返回
status
值说明:
- 200:成功
- 404:没有发现文件、查询或 URL
- 500:服务器产生内部错误
# 24.2 Ajax 的原理及优缺点
Ajax 的原理:相当于在用户和服务器之间加一个中间层(Ajax 引擎),使用户操作与服务器响应异步化。由客户端请求 Ajax 引擎,再由 Ajax 引擎请求服务器,服务器作出一系列响应之后返回给 Ajax 引擎,由 Ajax 引擎决定将这个结果写入到客户端的什么位置。实现页面无刷新更新数据。
- 优点:在不刷新整个页面的前提下与服务器通信维护数据。不会导致页面的重载可以把前端服务器的任务转嫁到客服端来处理,减轻服务器负担,节省带宽。
- 劣势:不支持返回上一次请求内容。对搜索引擎的支持比较弱(百度在国内搜索引擎的占有率最高,但是很不幸,它并不支持 Ajax 数据的爬取);不容易调试。
怎么解决呢?通过 location.hash
值来解决 Ajax 过程中导致的浏览器前进后退按键失效。
解决以前被人常遇到的重复加载的问题。主要比较前后的 hash 值,看其是否相等,在判断是否触发 Ajax。
function getData(url) {
var xhr = new XMLHttpRequest(); // 创建一个对象,创建一个异步调用的对象
xhr.open('get', url, true) // 设置一个 HTTP 请求,设置请求的方式,URL 以及验证身份
xhr.send() //发送一个 HTTP 请求
xhr.onreadystatechange = function () { //设置一个 HTTP 请求状态的函数
if (xhr.readyState == 4 && xhr.status ==200) {
console.log(xhr.responseText) // 获取异步调用返回的数据
}
}
}
2
3
4
5
6
7
8
9
10
- 适用场景:表单驱动的交互、深层次的树的导航、快速用户间响应、投票等场景、数据过滤和操纵场景、普通的文本输入场景
- 不适用场景:部门简单表单、搜索、基本导航、替换大量文本、对呈现的操纵
# 24.3 Ajax 和 Axios 的区别
Axios 是一个基于 Promise 的 HTTP 库,主要实现 Ajax 异步通信功能,用于向后端发起请求,还有在请求中做更多是可控功能。Axios 实现对 Ajax 的封装。
简单理解为:封装好的、基于 Promise 的发送请求的方法。
Axios 是通过 Promise 实现对 Ajax 技术的一种封装,就像 jQuery 对 Ajax 的封装一样,Axios 回来的数据是 Promise,Ajax 回来的数据是回调,Axios 比 Ajax 更好用更安全。
简单来说就是 Ajax 技术实现了局部数据的刷新,Axios 实现了对 Ajax 的封装;Axios 有的 Ajax 都有,Ajax 有的 Axios 不一定有。Axios 是 Ajax,Ajax 不止 Axios。
参考:
# 25 setTimeout
和 setInterval
setTimeout
用于延时执行一个函数,传入参数为 fn
(回调函数)、timeout
(延时时间)、...args
(执行参数)。
setInterval
用于开启一个不断执行的计时器,传入参数为 fn
(回调函数)、timeout
(间隔时间)、...args
(执行参数)。
这两者都是 宏任务,并在当前所有宏任务、微任务结束后执行。
# 25.1 返回值问题
浏览器下,这两者返回的是一个唯一 id
,通过 clearTimeout
或 clearInterval
可以清除。
node 下,这两者返回的是一个 Timeout
对象,同样通过 clearTimeout
或 clearInterval
可以清除。
值得注意的是,如果把 setTimeout
或 setInterval
的返回值赋给一个变量,那么清除延时器或定时器后,变量的值不变。如果需要判断变量的值(如防抖、节流场景),需要清除后主动将变量值设为 null
。
# 25.2 用setTimeout
实现setInterval
function mySetInterval(callback, timeout, ...arguments) {
const fn = () => {
callback(...arguments);
setTimeout(fn, timeout);
}
setTimeout(fn, timeout);
}
mySetInterval(() => {
console.log('hello');
}, 1000);
2
3
4
5
6
7
8
9
10
11
原理:在 fn
内递归调用 fn
,使 callback
一直执行。
扩展:如何实现 clearInterval
?
var timeWorker = {};
function mySetInterval(callback, timeout, ...arguments) {
var key = Symbol();
const fn = () => {
callback(...arguments);
timeWorker[key] = setTimeout(fn, timeout);
}
timeWorker[key] = setTimeout(fn, timeout);
return key;
}
function myClearInterval(key) {
if (key in timeWorker) {
clearTimeout(timeWorker[key]);
delete timeWorker[key];
}
}
const interval = mySetInterval(() => {
console.log('hello');
}, 1000);
setTimeout(() => {
myClearInterval(interval);
}, 5500);
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
原理:因为 setTimeout
返回一个唯一 id
(浏览器下,node 下是一个对象),我们可以模拟这个过程,用一个唯一 id
(比如 Symbol
)记录下最后一个定时器并记录到一个全局变量中。清除定时器时,取出最后一个定时器将其清除。
# 26 事件循环
JS 的并发模型基于事件循环(Event Loop),不同于 C、Java 等语言。
一个 JS 运行时包含了一个待处理的消息队列,每一个消息都关联着一个用于处理这个消息的函数。
在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息,为此,这个消息会被移除队列,并作为输入参数调用与之关联的函数。调用一个函数会为其创造一个新的栈帧。函数的处理会一直进行到执行栈再次为空为止,然后事件循环会处理队列中的下一个消息(如果还有)。
下面代码输出什么?
const ms = new Date().getTime();
setTimeout(() => {
console.log("Ran after " + (new Date().getTime() - ms) + " ms");
}, 500);
while (true) {
if (new Date().getTime() - ms >= 2000) {
console.log("Good, looped for 2 second");
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
Good, looped for 2 second
Ran after 2006 ms
2
第3行,setTimeout
语句进入调用栈,这是一个延时器,因此将回调函数(第 4 行)加入 Web API,Web API 的作用是在 500 ms 后将回调函数的执行加入到宏任务队列。
紧接着执行 while
循环,500 ms 后 Web API 将回调函数的执行加入宏任务队列,但此时 while
循环仍在执行,调用栈未被清空,直到 2000 ms 后 while
循环退出,调用栈清空,取出宏任务队列中的回调函数并加入调用栈执行。
事件循环可以理解为以下循环:
- 主线程执行同步代码,执行过程中产生调用栈,如果有异步事件,则交给异步模块处理,如异步任务有结果,则异步模块在消息队列添加消息
- 如果同步任务完成(调用栈清空),主线程检查消息队列,如果消息队列不为空,取出头部的待处理消息,加入主线程
- 主线程重复以上过程
参考:
# 27 JS设计模式
待补充
# 27.1 单例
# 27.2 手写 EventEmitter
手写一个事件派发器,统一管理某一类型的回调,并在之后对特定类型进行触发,注意每一类型可能不止一个事件。
class EventEmitter {
constructor() {
this.listeners = {};
}
/**
* 注册事件
* @param {string} type 事件类型
* @param {function} fn 事件处理函数
*/
on(type, fn) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(fn);
}
/**
* 发布事件
* @param {string} type 事件类型
* @param {...any} args 事件参数
*/
emit(type, ...args) {
if (this.listeners[type]) {
this.listeners[type].forEach(fn => fn(...args));
}
}
/**
* 移除某个类型的一个事件
* @param {string} type 事件类型
* @param {function} fn 事件处理函数
*/
off(type, fn) {
if (this.listeners[type]) {
this.listeners[type] = this.listeners[type].filter(item => item !== fn);
if (this.listeners[type].length === 0) {
delete this.listeners[type];
}
}
}
/**
* 移除某个类型的所有事件
* @param {string} type 事件类型
*/
offAll(type) {
if (this.listeners[type]) {
delete this.listeners[type];
}
}
}
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
使用示例:
const ee = new EventEmitter();
ee.on('imagine', () => console.log('imagine'));
ee.emit('imagine'); // imagine
ee.on('imagine', (name, address) => console.log(`Hello, I am ${name} from ${address}`));
ee.emit('imagine', 'Jack', 'China'); // imagine Hello, I am Jack from China
const fn = () => console.log('I am fn');
ee.on('TestOff', fn);
ee.emit('TestOff'); // I am fn
ee.off('TestOff', fn);
ee.emit('TestOff'); // nothing
ee.offAll('imagine');
console.log(ee); // EventEmitter { listeners: {} }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16