JavaScript笔记

JavaScript笔记

1. JavaScript如何实现继承

1.1 原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

1
2
3
4
5
6
7
8
9
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child.prototype = new Parent();
console.log(new Child())

1.2 盗用构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent(){
this.name = 'parent';
}

Parent.prototype.getName = function () {
return this.name;
}

function Child(){
Parent.call(this);
this.type = 'child'
}

let child = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错

可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

1.3 组合继承

组合继承则将前两种方式继承起来。

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 Parent () {
this.name = 'parent';
this.play = [1, 2, 3];
}

Parent.prototype.getName = function () {
return this.name;
}
function Child() {
// 第二次调用 Parent()
Parent.call(this);
this.type = 'child';
}

// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;

var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // 不互相影响
console.log(s1.getName()); // 正常输出'parent'
console.log(s2.getName()); // 正常输出'parent'

1.4 原型式继承

这里主要借助Object.create方法实现普通对象的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};

let person1 = Object.create(parent);
person1.name = "tom";
person1.friends.push("jerry");

let person2 = Object.create(parent);
person2.friends.push("lucy");

console.log(person1.name); // tom
console.log(person1.name === person1.getName()); // true
console.log(person2.name); // parent
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]

这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。

1.5 寄生式继承

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};

function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}

let person = clone(parent);

console.log(person.getName()); // parent
console.log(person.getFriends()); // ["p1", "p2", "p3"]

其优缺点也很明显,跟上面讲的原型式继承一样。

1.6 寄生式组合继承

寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式。

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
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}

function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
Parent.call(this);
this.friends = 'child';
}

clone(Parent, Child);

Child.prototype.getFriends = function () {
return this.friends;
}

let person = new Child();
console.log(person); // Child {name: 'parent', play: Array(3), friends: 'child'}
console.log(person.getName()); // parent
console.log(person.getFriends()); // child

2. object.assign和扩展运算符的区别

  1. 两者都是浅拷贝,确切地说是对于对象实例的拷贝属于浅拷贝
  2. 对象合并,数组合并,Object.assign、connat的性能会比展开运算符“…”的性能高
  3. Object.assign会触发Proxy/Object.definedProperty的set方法,展开运算符“…”不会触发
  4. 合并对象、数组的时候,展开运算符放在前面的性能比放在后面的性能高

3. Web Storage API

3.1 sessionStorage

sessionStorage为每一个给定的源(origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。

  • 仅为会话存储数据,这意味着数据将一直存储到浏览器(或选项卡)关闭。
  • 数据永远不会被传输到服务器。
  • 存储限额大于 cookie(最大 5MB)。

3.2 localStorage

localStorage做同样的事情,但即使浏览器关闭并重新打开也仍然存在。

  • 存储的数据没有过期日期,只能通过 JavaScript、清除浏览器缓存或本地存储的数据来清除。
  • 存储限额是两者之间的最大值。

4. WebSocket

  • WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
  • 现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
  • HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

案例

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
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>

<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");

// 打开一个 web socket
var ws = new WebSocket("ws://localhost:9998/echo");

ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};

ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert("数据已接收...");
};

ws.onclose = function()
{
// 关闭 websocket
alert("连接已关闭...");
};
}

else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>

</head>
<body>

<div id="sse">
<a href="javascript:WebSocketTest()">运行 WebSocket</a>
</div>

</body>
</html>

5. 前端本地存储的4种方法

  1. 用于浏览器和server的通讯
  2. 可设置失效时间,默认是浏览器关闭后失效
  3. 存放数据大小为4K左右
  4. 每次都会携带在HTTP头中,如果使用Cookie保存过多数据会带来性能的问题
  5. cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据

5.2 localStorage

  1. 除非被清除,否则永久保存
  2. 存放数据大小一般为5MB
  3. 仅在客户端(即浏览器)中保存,不参与和服务器的通信
  4. 浏览器可以设置是否可以访问数据,如果设置不允许会访问失败
  5. 兼容IE8以上浏览器
  6. 只能存储字符串类型,需要转成字符串存储

5.3 sessionStorage

  1. 仅在当前会话下有效,关闭tab页面或浏览器后被清除
  2. 存放数据大小一般为5MB
  3. 仅在客户端(即浏览器)中保存,不参与和服务器的通信

5.4 indexDB

MDN官网:
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
IndexedDB主要用来客户端存储大量数据而生的,我们都知道cookie、localstorage等存储方式都有存储大小限制。如果数据量很大,且都需要客户端存储时,那么就可以使用IndexedDB数据库。

使用场景:

  1. 数据可视化等界面,大量数据,每次请求会消耗很大性能。
  2. 即时聊天工具,大量消息需要存在本地。
  3. 其它存储方式容量不满足时,不得已使用IndexedDB

6. 函数柯里化

函数柯里化指的是一种将使用多个参数的一个函数,转换成一系列使用一个参数的函数的技术。
对于已经柯里化后的 _fn 函数来说,

  • 当接收的参数数量与原函数的形参数量相同时,执行原函数;
  • 当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。
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
function curry(fn, args) {
// 获取函数需要的参数长度
let length = fn.length;

args = args || [];

return function() {
let subArgs = [...args, ... arguments];
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回柯里化函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
}
}

// 需要柯里化的函数
function multiFn(a, b, c) {
return a * b * c;
}

// multi是柯里化之后的函数
let multi = curry(multiFn);
console.log(multi(2)(3)(4)); // 24
console.log(multi(2, 3, 4));
console.log(multi(2)(3, 4));
console.log(multi(2, 3)(4));
1
2
3
4
// ES6实现
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

7. 利用 Promise.race 和 Promise.all 解决网络过快 loading 闪烁问题

终极解决方式是将 Promise.all() 和 Promise.race() 搭配使用。先利用Promise.race()约束请求在超时时间内返回时就直接渲染,否则就固定展示一段时间的loading动画再渲染数据。即请求如果没有在 500ms 内返回则固定展示 1500ms 的loading。

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 resolvePromise(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`在${time}ms后返回成功Promise`);
}, time)
});
}

function rejectPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error(`在${time}ms后返回失败Promise`));
}, time)
});
}

function reqData() {
// 记录请求的状态
const axiosRequest = getData();
Promise.race([axiosRequest, rejectPromise(500)]).then((res) => {
// 成功意味着请求在固定时间内返回
}).catch((err) => {
// 超时,整体变成onrejected,展示loading
loading.value = true;
console.log(err.message);
Promise.all([axiosRequest, resolvePromise(1500)]).then((res) => {
// Promise.all执行结果返回的数组顺序是按传入顺序决定的
console.log(res[0]);
}).catch((err) => {
console.log(err);
}).finally(() => {
lodaing.value = false;
})
})
}

8. 链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class N {
constructor(value) {
this.value = value || 0;
}
add(num) {
this.value += num;
return this;
}
minus(num) {
this.value -= num;
return this;
}
get() {
console.log(this.value);
return this.value;
}
}

const n = new N(2);
n.add(198).minus(100).get(); // 100

9. 手写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
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
const PENDING = 'pending';
const FULLFILLED = 'fullfilled';
const REJECTED = 'rejected';

function MyPromise(fn) {
this.state = PENDING;
this.value = null;
const that = this;
this.resolvedCallbacks = [];
this.rejectedCallabcks = [];

function resolve(val) {
if (that.state === PENDING) {
that.state = FULLFILLED;
that.value = val;
that.resolvedCallbacks.map((cb) => {
cb(that.value);
})
}
}

function reject(val) {
if (that.state === PENDING) {
that.state = REJECTED;
that.value = val;
that.rejectedCallabcks.map((cb) => {
cb(that.value);
})
}
}

try {
fn(resolve, reject);
} catch (err) {
reject(err);
}
}

MyPromise.prototype.then = function (onFullfilled, onRejected) {
// 保存前一个promise的this
const self = this;
return new MyPromise((resolve, reject) => {
// 封装前一个promise成功时执行的函数
let fullfilled = () => {
try {
const result = onFullfilled(self.value);
return result instanceof MyPromise ? result.then(resolve, reject) : resolve(result);
} catch (err) {
reject(err);
}
}
// 封装前一个promise失败时执行的函数
let rejected = () => {
try {
const result = onReject(self.reason);
return result instanceof MyPromise ? result.then(resolve, reject) : reject(result);
} catch (err) {
reject(err)
}
}
switch (self.state) {
case PENDING:
self.resolvedCallbacks.push(fullfilled);
self.rejectedCallabcks.push(rejected);
break;
case FULLFILLED:
fullfilled();
break;
case REJECTED:
rejectted();
break;
}
})
}

let p = new MyPromise((resolve, reject) => {
resolve('OK');
})
p.then((res) => {
console.log(res, 'OK');
}, (err) => {
console.log(err, 'fail');
})

10. 执行上下文的创建

创建执行上下文有明确的几个步骤:

  1. 确定this,即我们所熟知的this绑定
  2. 创建词法环境组件。
  3. 创建变量环境组件。

10.1 确定this

在全局执行上下文中,this 总是指向全局对象。例如:浏览器环境下 this 指向 window 对象。
在函数执行上下文中,this 的值取决于函数的调用方式,如果被一个对象调用,那么 this 指向这个对象。否则(在浏览器中) this 一般指向全局对象 window 或者 undefined (严格模式)。

10.2 创建词法环境组件

词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量(函数)的名称变量是对实际对象(包括函数类型对象)或原始值的引用
如:var name = 1;。标识符是 name,引用是 1
词法环境由
环境记录器
与对外部环境的引用两个组件组成:

  1. 环境记录器用于存储当前环境中的变量和函数声明的实际位置。
  2. 外部环境的引用对应着可以访问的其它外部环境。(所以子作用域可以访问父作用域)

10.3 创建变量环境

变量环境与词法环境十分相似。在 ES6 中,词法环境和变量环境的明显不同就是前者被用来存储函数声明和变量(let/const)的绑定,而后者只用来存储 var 变量的绑定

11. bind函数与call和apply的区别

11.1 bind

bind方法和call很相似,第一个参数也是this指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。bind返回的是一个永久改this指向的函数。

11.2 区别

共同点:

  1. 三者都可以改变函数的this对象指向。
  2. 三者第一个参数都是this要指向的对象,如果没有这个参数或参数为undefinednull,则默认指向全局window

区别:

  1. 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
  2. bind是返回绑定this之后的函数,apply和call是立即执行

12. new的实现原理

new 方法主要分为四个步骤:

  • 创建一个对象
  • 将构造函数中的this指向该对象
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _new(obj, ...rest){
// 基于obj的原型创建一个新的对象
const newObj = Object.create(obj.prototype);

// 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
const result = obj.apply(newObj, rest);

// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return typeof result === 'object' ? result : newObj;
}

// 使用
function obj(name, age) {
this.name = name;
this.age = age;
}
console.log(_new(obj, "zwx", 18));

13. JavaScript脚本延迟加载的方式与区别

13.1 默认<script>

让我们首先定义<script>没有任何属性的情况。HTML 文件将被解析,直到脚本文件被命中,此时解析将停止,并且将发出请求来获取该文件(如果它是外部的)。然后将在恢复解析之前执行该脚本。

13.2 异步加载<script async>

async在 HTML 解析期间下载文件,并在下载完成后暂停 HTML 解析器以执行该文件。

13.3延迟加载<script defer>

defer在 HTML 解析期间下载文件,并且仅在解析器完成后才执行它。defer脚本还保证按照它们在文档中出现的顺序执行。

14. Ajax、Fetch、Axios三者的区别

14.1 Ajax

它的全称是:Asynchronous JavaScript And XML,翻译过来就是“异步的 Javascript 和 XML”。

Ajax 是一个技术统称,是一个概念模型,它囊括了很多技术,并不特指某一技术,它很重要的特性之一就是让页面实现局部刷新,无需重载整个页面。

简单来说,Ajax 是一种思想,XMLHttpRequest 只是实现 Ajax 的一种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<script>
function ajax(url) {
const xhr = new XMLHttpRequest();
xhr.open("get", url, false);
xhr.onreadystatechange = function () {
// 异步回调函数
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.info("响应结果", xhr.response)
}
}
}
xhr.send(null);
}
ajax('https://smallpig.site/api/category/getCategory')
</script>
</body>

我们使用这种方式实现网络请求时,如果请求内部又包含请求,以此循环,就会出现回调地狱,这也是一个诟病,后来才催生了更加优雅的请求方式。

14.2 Fetch

Fetch 是在 ES6 出现的,它使用了 ES6 提出的 promise 对象。它是 XMLHttpRequest 的替代品。

特点:

  • 使用 promise,不使用回调函数。
  • 采用模块化设计,比如 rep、res 等对象分散开来,比较友好。
  • 通过数据流对象处理数据,可以提高网站性能。
1
2
3
4
5
6
7
8
9
10
<body>
<script>
function ajaxFetch(url) {
fetch(url).then(res => res.json()).then(data => {
console.info(data)
})
}
ajaxFetch('https://smallpig.site/api/category/getCategory')
</script>
</body>

上段代码利用 Fetch 发送了一个最简单的 get 请求,其中最重要的特点之一就是采用了.then 链式调用的方式处理结果,这样不仅利于代码的可读,而且也解决了回调地狱的问题。

14.3 Axios

Axios 是一个基于 promise 封装的网络请求库,它是基于 XHR 进行二次封装。

特点:

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

所以说,Axios 可以说是 XHR 的一个子集,而 XHR 又是 Ajax 的一个子集。

1
2
3
4
5
6
7
8
9
// 发送 POST 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})

14.4 总结

作者

zwx

发布于

2024-04-21

更新于

2024-04-21

许可协议

评论