JS杂谈
# ⭕️ 关于 arguments 的使用
# ... 与 arguments
// 函数内使用arguments对象
function log() {
console.log([...arguments]);
console.log(...arguments);
}
log(1, 2, 3); // [1,2,3] 1 2 3
// 参数不定长时写的arguments与arguments对象无关 仅为一个形参名字
// 可以是 arguments 也可以是 args
function log2(a, ...args) {
console.log(a);
console.log(args);
console.log(...args);
}
log2(1, 2, 3); // 1 [2,3] 2 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
参数写成...args 时 在函数内部使用 args[index]即可访问对应参数
...args 会自动将我们传入的多个参数变为一个数组
# [].slice.call(arguments)
[].slice.call(arguments)
等效于Array.prototype.slice.call(arguments)
- 目的:让类数组对象 arguments 使用数组方法 slice(把类数组对象转为数组对象)
- 返回值:参数组成的数组
- 原理:
- slice()返回未改变的数组
- .call(arguments)让 this 指向 arguments 对象
由此可得 [].shift.call(arguments) 则可返回传入参数的第一项
该方法会推出 args 中的第一项
# ⭕️ 函数柯里化和反柯里化
# 柯里化
柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数
# 例子
实现add(1)(2, 3)(4)() = 10
的效果
- 传入参数时,代码不执行输出结果,而是先记忆起来
- 当传入空的参数时,代表可以进行真正的运算
function currying(fn) {
var allArgs = [];
return function next() {
var args = [].slice.call(arguments);
if (args.length > 0) {
allArgs = allArgs.concat(args);
return next;
} else {
return fn.apply(null, allArgs);
}
};
}
var add = currying(function() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 解析
- 使用闭包记忆传入参数
- 判断触发函数执行条件
function currying(fn) {
var allArgs = []; // 用来接收参数 闭包存储
return function next() {
var args = [].slice.call(arguments);
// 判断是否执行计算
if (args.length > 0) {
allArgs = allArgs.concat(args); // 收集传入的参数,进行缓存
return next;
} else {
return fn.apply(null, allArgs); // 符合执行条件,执行计算
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 总结
- 逐步接收参数,并缓存供后期计算使用
- 不立即计算,延后执行
- 符合计算条件时将缓存参数统一传给执行方法
# ⭕️ 关于前端路由
# 前端路由的由来
SPA 中用户的交互通过 js 改版 html 内容来实现,页面本身的 url 并没有变化,这导致两个问题:
- SPA 无法记住用户的操作记录
- 只有一个 url 对 SEO 不友好
🔥 前端路由就是为了解决上述问题出现的
# 什么是前端路由
保证只有一个 HTML 页面,且与用户交互时不刷新跳转的同时,为每一个视图匹配一个特殊的 url。
需要做到以下两点:
- 改变 url 且不让浏览器发送请求
- 可以监听到 url 变化
接下来介绍
# hash 模式
# 什么是 hash?
www.baidu.com/#hash中#hash 即为我们期望的 hash 值。
hash 值的变化不会导致浏览器像服务器发送请求,而且 hash 改变会触发 hashchange 事件。(实现核心)
# 使用到的 api
window.location.hash = "#..."; // 设置hash
let hash = window.loaction.hash; // 获取hash
window.addEventListener("hashchange", e => {}); // 监听hash
2
3
# 实现
class HashRouter {
constructor() {
//用于存储不同hash值对应的回调函数
this.routers = {};
window.addEventListener("hashchange", this.load.bind(this), false);
}
//用于注册每个视图
register(hash, callback = function() {}) {
this.routers[hash] = callback;
}
//用于注册首页
registerIndex(callback = function() {}) {
this.routers["index"] = callback;
}
//用于处理视图未找到的情况
registerNotFound(callback = function() {}) {
this.routers["404"] = callback;
}
//用于处理异常情况
registerError(callback = function() {}) {
this.routers["error"] = callback;
}
//用于调用不同视图的回调函数
load() {
let hash = location.hash.slice(1),
handler;
//没有hash 默认为首页
if (!hash) {
handler = this.routers.index;
}
//未找到对应hash值
else if (!this.routers.hasOwnProperty(hash)) {
handler = this.routers["404"] || function() {};
} else {
handler = this.routers[hash];
}
//执行注册的回调函数
try {
handler.apply(this);
} catch (e) {
console.error(e);
(this.routers["error"] || function() {}).call(this, e);
}
}
}
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
# history 模式
# 介绍
在 HTML5 之前,浏览器已经有了 history 对象,但在早期的 history 中只能用于多页面的跳转
在 H5 规范中新增了以下几个 api:
history.pushState(); // 添加新的状态到历史状态栈
history.replaceState(); // 用新的状态代替当前状态
history.state; // 返回当前状态对象
2
3
history.pushState() 和 history.replaceState() 的区别在于:
- history.pushState() 在保留现有历史记录的同时,将 url 加入到历史记录中。
- history.replaceState() 会将历史记录中的当前页面历史替换为 url。
🔥因为 history 改变不会触发任何事件,所以我们无法直接监听 history。因此我们需要将所有可能触发 history 改变的情况一一进行拦截并监听
对于 SPA 中 history 模式,url 改变只能由以下四种方式引起:
- 点击浏览器的前进或后退按钮
- 点击 a 标签
- 在 JS 代码中触发 history.pushState 函数
- 在 JS 代码中触发 history.replaceState 函数
# 实现
class HistoryRouter {
constructor() {
//用于存储不同path值对应的回调函数
this.routers = {};
this.listenPopState();
this.listenLink();
}
//监听popstate
listenPopState() {
window.addEventListener(
"popstate",
e => {
let state = e.state || {},
path = state.path || "";
this.dealPathHandler(path);
},
false
);
}
//全局监听A链接
listenLink() {
window.addEventListener(
"click",
e => {
let dom = e.target;
if (dom.tagName.toUpperCase() === "A" && dom.getAttribute("href")) {
e.preventDefault();
this.assign(dom.getAttribute("href"));
}
},
false
);
}
//用于首次进入页面时调用
load() {
let path = location.pathname;
this.dealPathHandler(path);
}
//用于注册每个视图
register(path, callback = function() {}) {
this.routers[path] = callback;
}
//用于注册首页
registerIndex(callback = function() {}) {
this.routers["/"] = callback;
}
//用于处理视图未找到的情况
registerNotFound(callback = function() {}) {
this.routers["404"] = callback;
}
//用于处理异常情况
registerError(callback = function() {}) {
this.routers["error"] = callback;
}
//跳转到path
assign(path) {
history.pushState({ path }, null, path);
this.dealPathHandler(path);
}
//替换为path
replace(path) {
history.replaceState({ path }, null, path);
this.dealPathHandler(path);
}
//通用处理 path 调用回调函数
dealPathHandler(path) {
let handler;
//没有对应path
if (!this.routers.hasOwnProperty(path)) {
handler = this.routers["404"] || function() {};
}
//有对应path
else {
handler = this.routers[path];
}
try {
handler.call(this);
} catch (e) {
console.error(e);
(this.routers["error"] || function() {}).call(this, e);
}
}
}
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
# hash 🆚 history
# ⭕️ EventLoop 深度解析
# 面试题
new Promise((resolve, reject) => {
console.log("promise1", 1);
resolve();
})
.then(() => {
console.log("then11", 2);
new Promise((resolve, reject) => {
console.log("promise2", 3);
resolve();
})
.then(() => {
console.log("then21", 4);
new Promise((resolve, reject) => {
console.log("promise3", 5);
resolve();
})
.then(() => {
console.log("then31", 7);
})
.then(() => {
console.log("then32", 9);
});
})
.then(() => {
console.log("then22", 8);
});
})
.then(() => {
console.log("then12", 6);
});
// 1 2 3 4 5 6 7 8 9
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
解析:
- 输出 1 -> then11 入队
队伍:【then11】 - then11 出队 -> 输出 2 -> 输出 3 -> then21 入队 -> then12 入队
队伍:【then21 then12】 - then21 出队 -> 输出 4 -> 输出 5 -> then31 入队 -> then22 入队
队伍:【then12 then31 then22】 - then12 出队 -> 输出 6
队伍:【then31 then22】 - then31 出队 -> 输出 7 -> then32 入队
队伍:【then22 then32】 - then22 出队 -> 输出 8 -> then32 出队 -> 输出 9
process.nextTick
优先级高于Promise.then
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("settimeout");
});
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
setImmediate(() => {
console.log("setImmediate");
});
process.nextTick(() => {
console.log("process");
});
console.log("script end");
// [script start,async1 start,async2,promise1,script end,process,async1 end,promise2,setTimeout,setImmediate]
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
# ⭕️requestAnimationFrame
随着技术与设备的发展,用户的终端对动画的表现能力越来越强,更多的场景开始大量使用动画。在 Web 应用中,实现动画效果的方法比较多,JavaScript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的 API,即 requestAnimationFrame
# 详解隐式转换
# 转字符串
注意:Object.prototype.toString([])
与 Array.prototype.toString([])
返回值不同
- 数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的 Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",
- 空数组[]转为空字符串,数组中的 null 或 undefined,会被当做空字符串处理
- 普通对象:转为字符串相当于直接使用
Object.prototype.toString()
,返回"[object Object]"
String(null); // 'null'
String(undefined); // 'undefined'
String(true); // 'true'
String(10); // '10'
String(1e21); // '1e+21'
String([1, 2, 3]); // '1,2,3'
String([]); // ''
String([null]); // ''
String([1, undefined, 3]); // '1,,3'
String({}); // '[object Objecr]'
2
3
4
5
6
7
8
9
10
# 转数字
Number(null); // 0
Number(undefined); // NaN
Number("10"); // 10
Number("10a"); // NaN
Number(""); // 0
Number(true); // 1
Number(false); // 0
Number([]); // 0
Number(["1"]); // 1
Number({}); // NaN
2
3
4
5
6
7
8
9
10
# 转布尔值
js 中的假值只有false、null、undefined、空字符、0、NaN
,其它值转为布尔型都为 true。
# 对象转原始 toPrimitive
- 先调用 valueOf
- 再调用 toString
数组调用 valueOf 返回本身
# 宽松相等的隐式转换规则
- 布尔 🆚 其他:布尔 -> 数字
- 数字 🆚 字符串:字符串 -> 数字
- 对象 🆚 原始:对象执行 toPrimitive
# null&undefined
- null == undefined // true
- null/undefined -> false
- ES 规范:和其他所有值都不宽松相等
# 自测题
[] == ![] // true
// ![] = false -> 数组vs布尔
// false = 0
// [] = 0
[] == 0 // true
// 对象vs原始
// [].valueOf -> [] -> [].toString -> '0' -> 字符串vs数字 -> 0 == 0
[2] == 2 // true
// 对象vs原始
// [].valueOf -> [] -> [].toString -> '2' -> 字符串vs数字 -> 2 == 2
['0'] == false // true
// 对象vs原始
// [].valueOf -> [] -> [].toString -> '0' -> 字符串vs布尔
// false -> 0 -> 字符串vs数字
'0' == false // true
// 字符串vs布尔
// false -> 0
[] == false // true
// 对象vs原始
// [] -> '0' -> 转为上题
[null] == 0 // true
// 对象vs原始
// [null] -> '' -> 字符串vs数字
// '' -> 0 -> 0 == 0
null == 0 // false
// 规范
[null] == false // true
// [null] -> '' -> 0
// false -> 0
null == false // false
// 规范
[undefined] == false // true
// [undefined] -> '' -> 0
// false -> 0
undefined == false // false
// 规范
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
# 箭头函数
# 没有 this
箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。
eg:
<button id="button">点击变色</button>
function Button(id) {
this.element = document.querySelector("#" + id);
this.bindEvent();
}
Button.prototype.bindEvent = function() {
this.element.addEventListener("click", this.setBgColor, false);
};
Button.prototype.setBgColor = function() {
// 如不绑定this 该函数内部this为元素引用
this.element.style.backgroundColor = "#1abc9c";
};
var button = new Button("button");
// Uncaught TypeError: Cannot read property 'style' of undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
原因:
**当使用 addEventListener() 为一个元素注册事件的时候,事件函数里的 this 值是该元素的引用。**即<button id="button">123</button>
解决方法:
.bindEvent 中的 this 是我们正确想要的,即 Button 的实例Button {element: button#button}
。所以我们需要将 addEventListener 中的事件函数 this 强制绑定为.bindEvent 中的 this
- 方法 1 使用 bind:
this.setBgColor.bind(this)
- 方法 2 使用箭头函数:
() => this.setBgColor()
# 没有 arguments
箭头函数可以访问外围函数的 arguments 对象
function constant() {
return () => arguments[0];
}
var result = constant(1);
console.log(result()); // 1
2
3
4
5
6
# 不能通过 new 关键字调用
JavaScript 函数有两个内部方法:[[Call]]
和 [[Construct]]
。
当通过 new 调用函数时,执行 [[Construct]]
方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。
当直接调用的时候,执行 [[Call]]
方法,直接执行函数体。
箭头函数并没有 [[Construct]]
方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。
# 没有原型
由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。
# 没有 super
连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。
# 自执行函数
(function() {
console.log(1);
})();
// 或
(function() {
console.log(1);
})();
// 用箭头函数简写
(() => {
console.log(1);
})();
2
3
4
5
6
7
8
9
10
11