# js 运行机制
参考:
- https://juejin.cn/post/6844904050543034376#comment
- https://jiegiser.github.io/note/javascript/%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E6%9C%BA%E5%88%B6.html
# 闭包是什么,有什么应用场景
# 闭包是什么
其实在全局上下文中创建函数都会创建一个对应的闭包,存储了函数定义创建时作用域中的所有变量。只不过在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。
当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。
参考:https://juejin.cn/post/6844903858636849159#comment
# 闭包的应用场景
- 模仿块级作用域
比如我们可以使用闭包能使下面的代码按照我们预期的进行执行(每隔 1s 打印 0,1,2,3,4)。
for(var i = 0; i < 5; i++) {
(function(j){
setTimeout(() => {
console.log(j)
}, j * 1000)
})(i)
}
- 私有变量
JavaScript 中没有私有成员的概念,所有属性都是公有的。但是有私有变量的概念,任何在函数中定义的变量,都可以认为是私有变量,因为在函数的外部不能访问这些变量。私有变量包括函数的参数,局部变量和函数内部定义的其他函数。
function createCounter() {
let counter = 0
const myFunction = function() {
counter = counter + 1
return counter
}
return myFunction
}
const increment = createCounter()
const c1 = increment()
const c2 = increment()
const c3 = increment()
console.log('example increment', c1, c2, c3)
- 静态私有变量
(function() {
var name = '';
Person = function(value) {
name = value;
}
Person.prototype.getName = function() {
return name;
}
Person.prototype.setName = function(value) {
name = value;
}
})()
var person1 = new Person('xiaoming')
console.log(person1.getName()) // xiaoming
person1.setName('xiaohong')
console.log(person1.getName()) // xiaohong
var person2 = new Person('luckyStar')
console.log(person1.getName()) // luckyStar
console.log(person2.getName()) // luckyStar
上面代码通过一个匿名函数实现块级作用域,在块级作用域中 变量 name 只能在该作用域中访问,同样的通过闭包(作用域链)的方式实现 getName 和 setName 来访问 name, 而 getName 和 setName 又是原型对象的方法,所以它们成了 Person 实例的共享方法。 这种模式下,name 就变成了一个静态的、由所有实例共享的属性。在一个实例上调用 setName() 会影响所有的实例。
- 模块模式
var singleton = function() {
var privateVarible = 10
function privateFunction() {
return false
}
return {
publicProperty: true,
publicMethod: function() {
privateVarible++
return privateFunction()
}
}
}
# 什么是发布订阅模式
说发布订阅模式之前,先看看观察者模式:
# 观察者模式
观察者模式说起来很简单,我们希望对一个目标进行 observe,当这个目标发生变化的时候告诉我们。其实这听上去和回调函数很像,其实回调函数就是一种特殊的观察者模式。真正的观察者模式需要是一对多的,被观察者 Subject 会管理一个观察者列表,当这个 Subject 发生某个变化的时候会通知观察者列表中的所有观察者。
如下模拟代码:
// 观察着
class Observer {
constructor(){}
update(val) {
console.log('val', val)
}
}
class ObserverList {
constructor() {
this.observerList = []
}
add(observer) {
this.observerList.push(observer)
}
remove(observer) {
this.observerList = this.observerList.filter(i => i!== observer )
}
count() {
return this.observerList.length
}
get(i) {
return this.observerList[i]
}
}
// 目标
class Subject {
constructor() {
this.observers = new ObserverList()
}
addObserver(observer) {
this.observers.add(observer)
}
removeObserver(observer) {
this.observers.remove(observer)
}
notify(...args) {
let obCount = this.observers.count()
for (let i = 0; i < obCount; i++) {
this.observers.get(i).update(...args)
}
}
}
const DouYu = new Subject()
const zs = new Observer()
DouYu.addObserver(zs)
DouYu.notify([122])
这和我们经常使用的 DOM 事件模式完全相同。我们的 DOM 事件也就是一个观察者模式的实现。当我们为一个元素绑定事件的时候,我们就相当于进行了 Subscribe 订阅,成为了一个观察者。当对应的事件触发的时候,被观察的元素会通知我们,执行我们的回调函数(所谓的通知其实就是执行观察者中的对应方法,并不是真的发个通知)。我们也可以绑定多个 Observer,当事件触发的时候,所有的观察者都会被通知。DOM 提供了我们订阅 addEventListener,和取消订阅 removeEventListener 的 API。
# 发布订阅模式
有了观察者模式,为什么又要有发布订阅模式呢?观察者模式中,观察者和目标是依赖的,耦合性很强。
我们可以想象一个场景,我们有多个目标要进行监听,但是这些目标事件触发后执行的逻辑其实是相同的,如果是使用观察者模式我们不得不对每一个目标都进行绑定,并且每一个目标都要事先一套上面 Subject 的逻辑。这样处理在系统越来越复杂的情况下代码的逻辑会非常混乱,也非常难以管理,并且有非常多冗余的部分。
为了解决这个问题,就有了发布订阅模式。他们的本质都是一样的,都是对于未来会发生的事件进行一个监听,当事件发生了,通知监听的对象执行对应的方法。但是他们实现的逻辑不同。
在发布订阅模式中,添加了一个事件通道,发布者和订阅者不在直接进行交互。所有的注册,解绑,发布都是通过 Event Channel 来实现的。也就是说我们把观察者模式中的逻辑抽象出来,形成了一个单独的模块。
现在整个的逻辑大概是这样:我们不再是对某个对象进行监听,而是告诉 Event Channel,我想要注册一个名叫 type 的事件,当这个事件触发以后,请执行 fn 函数。事件中心将我的注册信息进行保存。当有一个模块想要执行订阅者的对应方法的时候,只要告诉 Event Channel,我想要触发 type 事件,并且传入参数 arg1, arg2 ...。Event Channel 就会找到对应事件对应的 fn 传入 arg1, arg2 ... 并执行。
我们可以看到 Event Channel 就像一个消息中介,调度中心,它让发布者和订阅者之间完全解耦,它们甚至不知道对方的存在。我们将所有的订阅发布行为进行集中的管理,并且能够定制我们的订阅发布行为,让不同模块之间的通信业变得非常便捷。我们看下面的模拟实现发布订阅代码:
class PubSub {
constructor() {
this.subscribers = {}
}
subscribe(type, fn) {
let listeners = this.subscribers[type] || [];
listeners.push(fn);
}
unsubscribe(type, fn) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
this.subscribers[type] = listeners.filter(v => v !== fn);
}
publish(type, ...args) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
listeners.forEach(fn => fn(...args));
}
}
let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val));
ob.publish('add', 1);
比较观察者模式和发布订阅模式的代码我们可以发现,观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,会造成代码的冗余。而发布订阅模式则统一由调度中心处理,消除了发布者和订阅者之间的依赖。
我们可以结合生活中的实例来帮助你理解,比如你周五想约朋友去吃饭,你不知道谁有空,按观察者模式的逻辑来说,你得给每个可能的朋友发条信息:如果触发有空事件请给我打电话(注册)
,当朋友触发有空
事件的时候,将会对你进行通知(执行打电话
方法)。如果是订阅发布模式的话逻辑就是,你向朋友圈
(这里相当于 Event Channel) 注册了一个事件,名字叫有空
,方法是打电话
,当有朋友想要参与聚会的时候,会告诉朋友圈 (Event Channel)
,你要发布有空
事件,参数是185xxxxxxxx
,朋友圈用这个参数执行了打电话
方法。
- 实现
正因为观察订阅模式的这种机制,它成了很多框架和库用来实现模块之间通信的方式。比如 Vue 的 Event(bus),React 的 Event 模块,他们都用来实现非父子组件的通信。实际上几乎所有的模块通信都是基于类似的模式,包括安卓开发中的Event Bus,Node.js 中的 Event 模块( Node 中几乎所有的模块都依赖于 Event,包括不限于 http、stream、buffer、fs 等)。
这一小节我们就仿照 NodeJS 的 Event API 实现一个简单的 Event 库。
我们的大致需求是:实现一个 Emitter 类,该类能够实现事件的订阅,解绑,发布。事件的存储使用 Map。对于同一个类型的事件,支持多次绑定(即传入多个方法),当事件发布的时候,这些方法都将执行。
class Emitter {
constructor() {
this._event = this._event || new Map()
this.maxListeners = 10
}
addEventListener(type, fn) {
const handler = this._event.get(type)
if (!handler) {
this._event.set(type, fn)
} else {
if (handler && typeof handler === 'function') {
this._event.set(type, [handler, fn])
} else {
handler.push(fn)
}
}
}
removeEventListener(type, fn) {
const handler = this._event.get(type)
if (handler && typeof handler === 'function') {
if (handler === fn) this._event.delete(type)
} else {
let newHandler = handler.filter(v => v !== fn)
if (newHandler.length === 1) {
this._event.set(type, newHandler[0])
} else {
this._event.set(type, newHandler)
}
}
}
emit(type, ...args) {
const handler = this._event.get(type)
if (Array.isArray(handler)) {
handler.forEach(fn => {
fn.apply(this, args)
})
} else {
handler.apply(this, args)
}
return true
}
}
let emitter = new Emitter()
emitter.addEventListener('change', obj => {
console.log(`name is ${obj.name}`)
})
emitter.addEventListener('change', obj => {
console.log(`age is ${obj.age}`)
})
emitter.addEventListener('change', obj => {
console.log(`sex is ${obj.sex}`)
})
function site(obj) {
console.log(`site is ${obj.site}`)
}
emitter.addEventListener('change', site)
emitter.emit('change', {
name: 'jiegiser',
age: 18,
sex: 'male',
site: '123.com'
})
emitter.removeEventListener('change', site)
emitter.emit('change', {
name: 'jiegiser',
age: 18,
sex: 'male',
site: '123.com'
})
class Emitter {
constructor() {
this._event = this._event || new Map();
this.maxListeners = 10;
}
addEventListener(type, fn) {
const handler = this._event.get(type);
if (!handler) {
this._event.set(type, fn)
} else {
if (handler && typeof handler === 'function') {
this._event.set(type, [handler, fn])
} else {
handler.push(fn);
}
}
}
removeEventListener(type, fn) {
const handler = this._event.get(type);
if (handler && typeof handler === 'function') {
if (handler === fn) this._event.delete(type);
} else {
let newHandler = handler.filter(v => v !== fn)
if (newHandler.length === 1) {
this._event.set(type, newHandler[0])
} else {
this._event.set(type, newHandler)
}
}
}
emit(type, ...args) {
const handler = this._event.get(type);
if (Array.isArray(handler)) {
handler.forEach(fn => {
fn.apply(this, args)
})
} else {
handler.apply(this, args);
}
return true;
}
}
let emitter = new Emitter();
emitter.addEventListener('change', obj => {
console.log(`name is ${obj.name}.`);
})
emitter.addEventListener('change', obj => {
console.log(`age is ${obj.age}.`);
})
emitter.addEventListener('change', obj => {
console.log(`sex is ${obj.sex}.`);
})
function site(obj) {
console.log(`site is ${obj.site}`)
}
emitter.addEventListener('change', site)
emitter.emit('change', {
name: 'clloz',
age: 28,
sex: 'male',
site: 'clloz.com'
})
//name is clloz.
//age is 28.
//sex is male.
//site is clloz.com
emitter.removeEventListener('change', site)
emitter.emit('change', {
name: 'clloz',
age: 28,
sex: 'male',
site: 'clloz.com'
})
//name is clloz.
//age is 28.
//sex is male.
参考:https://www.clloz.com/programming/front-end/js/2020/10/18/observer-pub-sub-pattern/
# 如何解决跨域
- 首先得知道什么是跨域:
在前端领域中,跨域是指浏览器允许向服务器发送跨域请求,从而克服 Ajax 只能同源使用的限制。
- 什么是同源策略
同源策略是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。域名和域名对应相同 ip 也是不行;
同源策略限制以下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS 对象无法获得
- AJAX 请求不能发送
下面是几种跨域解决方法
# JSONP 跨域
jsonp 的原理就是利用<script>
标签没有跨域限制,通过<script>
标签 src 属性,发送带有 callback 参数的 GET 请求,服务端
将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。
原生 JS 实现:
<script>
var script = document.createElement('script')
script.type = 'text/javascript'
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'
document.head.appendChild(script)
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res))
}
</script>
服务端返回如下(返回时即执行全局函数):
handleCallback({'success': true, 'user': 'admin'})
axios 实现:
this.$http = axios
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
后端 node.js 代码:
var querystring = require('querystring')
var http = require('http')
var server = http.createServer()
server.on('request', function(req, res) {
var params = querystring.parse(req.url.split('?')[1])
var fn = params.callback;
// jsonp 返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' })
res.write(fn + '(' + JSON.stringify(params) + ')')
res.end();
});
server.listen('8080')
console.log('Server is running at port 8080...')
jsonp 的缺点:只能发送 get 一种请求。
# 跨域资源共享(CORS)
CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。浏览器将 CORS 跨域请求分为简单请求和非简单请求。 只要同时满足以下两个条件,就属于简单请求:
- 使用下列方法之一:
- head
- get
- post
- 请求的 Heder 是
- Accept
- Accept-Language
- Content-Language
- Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain
参考链接:https://juejin.cn/post/6844903882083024910
# 简单请求
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的头信息中,Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
CORS 请求设置的响应头字段,都以 Access-Control- 开头:
- Access-Control-Allow-Origin:必选;它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求。
- Access-Control-Allow-Credentials:可选;它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
- Access-Control-Expose-Headers:可选;CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers 里面指定。上面的例子指定,getResponseHeader('FooBar') 可以返回 FooBar 字段的值。
# 非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)。
- 预检请求
预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是 Origin,表示请求来自哪个源。除了Origin 字段,"预检"请求的头信息包括两个特殊字段。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..
- Access-Control-Request-Method:必选;用来列出浏览器的 CORS 请求会用到哪些 HTTP方 法,上例是 PUT。
- Access-Control-Request-Headers:可选;该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header。
- 预检请求的回应
服务器收到"预检"请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。HTTP 回应中,除了关键的是 Access-Control-Allow-Origin 字段,其他 CORS 相关字段如下:
- Access-Control-Allow-Methods:必选;它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
- Access-Control-Allow-Headers 如果浏览器请求包括 Access-Control-Request-Headers 字段,则Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
- Access-Control-Allow-Credentials:可选;该字段与简单请求时的含义相同。
- Access-Control-Max-Age:可选;用来指定本次预检请求的有效期,单位为秒。
# 前端示例
// IE8/9 需用 window.XDomainRequest 兼容
var xhr = new XMLHttpRequest()
// 前端设置是否带 cookie
xhr.withCredentials = true
xhr.open('post', 'http://www.domain2.com:8080/login', true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send('user=admin')
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText)
}
};
# 服务器端设置
var http = require('http')
var server = http.createServer()
var qs = require('querystring')
server.on('request', function(req, res) {
var postData = ''
// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送 Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com',// 允许访问的域(协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly的作用是让js无法读cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen('8080');
console.log('Server is running at port 8080...');
# nginx 代理跨域
nginx 代理跨域,实质和 CORS 跨域原理一样,通过配置文件设置请求响应头 Access-Control-Allow-Origin...等字段。
# nginx 配置解决 iconfont 跨域
浏览器跨域访问 js、css、img 等常规静态资源被同源策略许可,但 iconfont 字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
# nginx 反向代理接口跨域
跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。
实现思路:通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域访问。
nginx 具体配置:
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
# document.domain + iframe 跨域
此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。
- 父窗口:
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
- 子窗口:
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
# WebSocket 协议跨域
WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。 原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。
- 前端代码
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080')
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
})
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
})
})
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value)
}
</script>
- Nodejs socket 后台
var http = require('http')
var socket = require('socket.io')
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end()
})
server.listen('8080')
console.log('Server is running at port 8080...')
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg)
console.log('data from client: ---> ' + msg)
})
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.')
})
})
链接:https://juejin.cn/post/6844903882083024910
# 浏览器的缓存策略
首先对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。接下来的内容中我们将通过缓存位置、缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。
# 缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
# Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
# Memory Cache
Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢? 这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存:
![在这里插入图片描述](./img/http14.png
内存缓存中有一块重要的缓存资源是 preloader 相关指令(例如<link rel="prefetch">
)下载的资源。总所周知 preloader 的相关指令已经是页面优化的常见手段之一,它可以一边解析 js/css 文件,一边网络请求下一个资源。
需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值,同时资源的匹配也并非仅仅是对URL 做匹配,还可能会对 Content-Type,CORS 等其他特征做校验。
# Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。
# Push Cache
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
他有如下几个特点:
- 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
- 可以推送 no-cache 和 no-store 的资源
- 一旦连接被关闭,Push Cache 就被释放
- 多个页面可以使用同一个 HTTP/2 的连接,也就可以使用同一个 Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的 tab 标签使用同一个 HTTP 连接。
- Push Cache 中的缓存只能被使用一次
- 浏览器可以拒绝接受已经存在的资源推送
- 你可以给其他域名推送资源
如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。
那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。参考 http 缓存机制;
参考链接:https://www.jianshu.com/p/54cc04190252
# HTTP 缓存机制
其机制是根据 HTTP 报文的缓存标识进行的;根据是否需要向服务器重新发起 HTTP 请求将缓存过程分为两个部分,分别是强制缓存和协商缓存
# 缓存的过程
浏览器与服务器通信的方式为应答模式,即是:浏览器发起 HTTP 请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下 图:
由上图我们可以知道:
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。
# 强制缓存
强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:
不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如下图:
存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存(暂不分析),如下图
存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果,如下图
# 强制缓存的规则
当浏览器向服务器发起请求时,服务器会将缓存规则放入 HTTP 响应报文的 HTTP 头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是 Expires 和 Cache-Control,其中 Cache-Control 优先级比 Expires 高。
# Expires
Expires 是 HTTP/1.0 控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于 Expires 的值时,直接使用缓存结果。
到了 HTTP/1.1,Expire 已经被 Cache-Control 替代,原因在于 Expires 控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义,那么 Cache-Control 又是如何控制的呢?
# Cache-Control
在HTTP/1.1中,Cache-Control 是最重要的规则,主要用于控制网页缓存,主要取值为:
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:所有内容只有客户端可以缓存,Cache-Control 的默认取值
- no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
- no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
- max-age=xxx (xxx is numeric):缓存内容将在 xxx 秒后失效
接下来,我们直接看一个例子,如下:
由上面的例子我们可以知道:
- HTTP 响应报文中 Cache-Control 为 max-age=600,是相对值
- HTTP 响应报文中 expires 的时间值,是一个绝对值
由于 Cache-Control 的优先级比 expires,那么直接根据 Cache-Control 的值进行缓存,意思就是说在 600 秒内再次发起该请求,则会直接使用缓存结果,强制缓存生效。
注:在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control 相比于 expires 是更好的选择,所以同时存在时,只有 Cache-Control 生效。
了解强制缓存的过程后,我们拓展性的思考一下:浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?如下图:
这里我们以博客的请求为例,状态码为灰色的请求则代表使用了强制缓存,请求对应的 Size 值则代表该缓存存放的位置,分别为 from memory cache 和 from disk cache。
from memory cache 代表使用内存中的缓存,from disk cache 则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为 memory –> disk。
那么如何对于 memory cache 和 disk cache 缓存的进行选择,我们需要了解内存缓存 (from memory cache) 和硬盘缓存 (from disk cache),如下:
- 内存缓存(from memory cache):内存缓存具有两个特点,分别是快速读取和时效性:
- 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
- 时效性:一旦该进程关闭,则该进程的内存则会清空。
- 硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行 I/O 操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。
在浏览器中,浏览器会在 js 和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而 css 文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。
# 协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
协商缓存生效,返回 304,如下
协商缓存失效,返回200和请求结果结果,如下
同样,协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since 和 Etag / If-None-Match,其中 Etag / If-None-Match 的优先级比 Last-Modified / If-Modified-Since 高。
# Last-Modified / If-Modified-Since
Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间,如下。
If-Modified-Since 则是客户端再次发起该请求时,携带上次请求返回的 Last-Modified 值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件,如下。
# Etag / If-None-Match
Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下。
If-None-Match 是客户端再次发起该请求时,携带上次请求返回的唯一标识 Etag 值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有 If-None-Match,则会根据 If-None-Match 的字段值与该资源在服务器的 Etag 值做对比,一致则返回 304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为 200,如下。
注:Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有 Etag / If-None-Match 生效。
# 总结
强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存,主要过程如下:
参考链接:https://juejin.cn/post/6844903593275817998
# 输入 URL 后发生了什么
https://tridiamond.blog.csdn.net/article/details/108023150
# 怎么模拟一个队列,有多少种方式
# 实现一个 js 的反向链表
# 如果有一个列表非常长,分页后怎么做才能使得占据的内存最小
# this 的理解
# AMD,CMD、CommonJS 和 es6 的 esmodule 的区别
# AMD (requireJS)
异步模块加载机制;requireJS 实现了 AMD 规范;下面是 RequireJS 的基本用法:
- 通过 define 进行定义模块
// a.js
// define 可以传入三个参数,分别是字符串-模块名、数组-依赖模块、函数-回调函数
define(function() {
return 1
})
- 使用 require 导入定义的模块
// b.js
// 数组中声明需要加载的模块,可以是模块名、js文件路径
require(['a'], function(a) {
console.log(a) // 1
})
RequireJS 的特点:对于依赖的模块,AMD 推崇依赖前置,提前执行
。也就是说,在 define 方法里传入的依赖模块(数组),会在一开始就下载并执行。
# CMD (SeaJS)
CMD 是 SeaJS 在推广过程中生产的对模块定义的规范,在 Web 浏览器端的模块加载器中,SeaJS 与 RequireJS 并称,相对于 AMD,他是对于依赖的模块,推崇依赖就近,延迟执行
。也就是说,只有到 require 时依赖模块才执行。就是什么时候需要再进行模块的引入;
如下基本用法:
// a.js
/*
* define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串,
* factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
* define 也可以接受两个以上参数。字符串 id 表示模块标识,数组 deps 是模块依赖.
*/
define(function(require, exports, module) {
var $ = require('jquery');
exports.setColor = function() {
$('body').css('color','#333')
}
})
// b.js
// 数组中声明需要加载的模块,可以是模块名、js文件路径
seajs.use(['a'], function(a) {
$('#el').click(a.setColor)
})
# CommonJS
CommonJS 规范为 CommonJS 小组所提出,目的是弥补 JavaScript 在服务器端缺少模块化机制,NodeJS、webpack 都是基于该规范来实现的。
- 基本用法
// a.js
module.exports = function() {
console.log('a')
}
// b.js
const a = require('./a.js')
a() // 'a'
// a.js
exports.num = 1
exports.obj = {
name: 'obj'
}
// b2.js
const a = require('./a')
console.log(a2) // { num: 1, obj: { name: 'obj' } }
CommonJS的特点
所有代码都运行在模块作用域,不会污染全局作用域;
模块是同步加载的,即只有加载完成,才能执行后面的操作;
模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
require 返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值;
# ES6 Module
ES6 Module 是 ES6 中规定的模块体系,相比上面提到的规范, ES6 Module 有更多的优势,有望成为浏览器和服务器通用的模块解决方案。
ES6 Module 的基本用法:
// a.js
const name = 'lin'
const age = 13
const job = 'ninja'
export { name, age, job}
// b.js
import { name, age, job} from './a.js'
console.log(name, age, job) // lin 13 ninja
ES6 Module 的特点(对比 CommonJS )
- CommonJS 模块是运行时加载,因为它最终是输出一个对象,ES6 Module 是编译时输出接口;
- CommonJS 加载的是整个模块,将所有的接口全部加载进来,ES6 Module可以单独加载其中的某个接口;
- CommonJS 输出是值的拷贝,ES6 Module 输出的是值的引用,被输出模块的内部的改变会影响引用的改变;
- CommonJS this 指向当前模块,ES6 Module this 指向 undefined;
# 总结
- AMD 依赖前置,提前执行 (require.js),语法是 define,require;
- CMD 依赖就近,延迟执行 (sea.js),语法是 define,seajs.use([],cb);
- commonJS 语法 module.exports=fn或者exports.a=1; 通过 require('./a1') 来引入;
- commonJS 模块首次执行会被缓存,再次加载只返回缓存结果,require 返回的值是输出值的拷贝(对于引用类型是浅拷贝);
- es6 module 语法是 export {...}, import ...from..., export 输出的是值的引用。
- NodeJS、webpack 都是基于 commonJS 该规范来实现的
# Commonjs 和 es6 module 的区别
- commonJS 是同步导入,因用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大,后者是异步导入,因为用于浏览器端,需下载文件,如果采用同步导入对渲染会有很大影响;
- CommonJS 在导出时都是值的拷贝,就算导出的值变了,导入的值也不会变。如果想更新,必须重新导入一回;
- ES Module 导入导出的值指向同一个内存地址。所以,导入值也会随着导出值变化;
- ES Module 会编译成 require/exports 来执行;
- 一个导出的是值的拷贝,一个导出的是值的引用;
- 一个是同步导入,一个是异步导入;
参考:https://juejin.cn/post/6844903998089101319
# 原型链和原型
# 怎么实现一个 new
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.have = 'cat'
Person.prototype.getName = function() {
console.log(this.name)
}
function fakenews(Fn) {
let obj = Object.create(Fn.prototype)
Fn.apply(obj, Array.prototype.slice.call(arguments, 1))
return obj
}
const person = fakenews(Person, "张三", "20")
console.log(person.name)
console.log(person.age)
console.log(person.have)
person.getName()
# localstorage 和 sessionstorage 和 cookie
# 使用方式
- cookie
- 客户端操作
// 获取
document.cookie
// 设置 - 追加到第一位
document.cookie = 'jiegiser=123'
注意,如果服务器端设置了 HttpOnly,在前端是获取不到 cookie 的;
- 服务端设置
服务端向客户端发送的cookie(HTTP头,不带参数):
Set-Cookie: <cookie-name>=<cookie-value> (name可选)
服务端向客户端发送的cookie(HTTP头,带参数):
Set-Cookie: <cookie-name>=<cookie-value>;(可选参数1);(可选参数2)
- 可选参数
Expires=<date>
:cookie 的最长有效时间,若不设置则 cookie 生命期与会话期相同;
Max-Age=<non-zero-digit>
:cookie 生成后失效的秒数;
Domain=<domain-value>
:指定 cookie 可以送达的主机域名,若一级域名设置了则二级域名也能获取;
Path=<path-value>
:指定一个 URL,例如指定 path=/docs,则”/docs”、”/docs/Web/“、”/docs/Web/Http” 均满足匹配条件;
Secure
:必须在请求使用 SSL 或 HTTPS 协议的时候 cookie 才回被发送到服务器;
HttpOnly
:客户端无法更改 Cookie,客户端设置 cookie 时不能使用这个参数,一般是服务器端使用;
- 可选前缀
__Secure-
:以 __Secure- 为前缀的 cookie,必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS);
__Host-
:以 __Host- 为前缀的 cookie,必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS)。必须不能设置domian 属性(这样可以防止二级域名获取一级域名的 cookie),path 属性的值必须为 ”/“。
前缀使用示例:
Set-Cookie: __Secure-ID=123; Secure; Domain=example.com
Set-Cookie: __Host-ID=123; Secure; Path=/
document.cookie = "__Secure-KMKNKK=1234;Sercure"
document.cookie = "__Host-KMKNKK=1234;Sercure;path=/"
- localStorage 和 sessionStorage
localStorage 和 sessionStorage 所使用的方法是一样的,下面以 sessionStorage 为例子:
const name = 'jiegiser'
const age = 18
// 存储数据
sessionStorage.setItem(name, age)
sessionStorage.setItem('key', 'value')
// 获取到全部数据
const dataAll = sessionStorage.valueOf()
/**
* IsThisFirstTime_Log_From_LiveServer: "true"
jiegiser: "18"
length: 3
key: <unreadable>
*/
console.log(dataAll)
// 获取指定 key 数据
const dataSession = sessionStorage.getItem(name)
console.log(dataSession) // 18
// sessionStorage 是 js 对象,也可以使用 key 的方式来获取值
const dataSession2 = sessionStorage[name]
console.log(dataSession2) // 18
// 删除指定 key 数据
sessionStorage.removeItem(name)
console.log(dataAll)
// 清空缓存数据
sessionStorage.clear()
console.log(dataAll)
# 三者的异同
- 生命周期
- cookie:可设置失效时间,没有设置的话,默认是关闭浏览器后失效。
- localStorage:除非被手动清除,否则将会永久保存。
- sessionStorage:仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。
- 存放数据大小
- cookie:4KB 左右
- localStorage 和 sessionStorage:可以保存 5MB 的信息。
- http 请求
- cookie:每次都会携带在 HTTP 头中(在指定作用域情况下),如果使用 cookie 保存过多数据会带来性能问题
- localStorage 和 sessionStorage:仅在客户端(即浏览器)中保存,不参与和服务器的通信
- 作用域
- localStorage: 在同一个浏览器内,
同源文档
之间共享 localStorage 数据,可以互相读取、覆盖。 - sessionStorage: 与 localStorage 一样需要
同一浏览器同源文档
这一条件。不仅如此,sessionStorage 的作用域还被限定在了窗口中,也就是说,只有同一浏览器、同一窗口的同源文档才能共享数据。
例如你在浏览器中打开了两个相同地址的页面 A、B,虽然这两个页面的源完全相同,但是他们还是不能共享数据,因为他们是不同窗口中的。但是如果是一个窗口中,有两个同源的 iframe 元素的话,这两个 iframe 的 sessionStorage 是可以互通的。
- 易用性
- cookie:需要程序员自己封装,原生的 Cookie 接口不友好;
- localStorage 和 sessionStorage:原生接口可以接受,亦可再次封装来对 Object 和 Array 有更好的支持;
# 能设置或读取子域的 cookie 吗?
不行! 只能向当前域或者更高级域设置cookie 例如 client.com 不能向 a.client.com 设置 cookie, 而 a.client.com 可以向 client.com 设置 cookie
读取 cookie 情况同上
# 客户端设置 cookie 与服务端设置 cookie 有什么区别?
- 无论是客户端还是服务端, 都只能向自己的域或者更高级域设置 cookie;例如 client.com 不能向 server.com 设置 cookie, 同样 server.com 也不能向 client.com 设置 cookie;
- 服务端可以设置 httpOnly: true, 带有该属性的 cookie 客户端无法读取;
- 客户端只会带上与请求同域的 cookie, 例如 client.com/index.html 会带上 client.com 的 cookie, server.com/app.js 会带上 server.com 的 cookie, 并且也会带上 httpOnly 的 cookie;
- 但是, 如果是向服务端的 ajax 请求, 则不会带上 cookie;
# 同域/跨域 ajax 请求到底会不会带上 cookie?
这个问题与你发起 ajax 请求的方式有关:
fetch 在默认情况下, 不管是同域还是跨域 ajax 请求都不会带上 cookie, 只有当设置了 credentials 时才会带上该 ajax 请求所在域的 cookie, 服务端需要设置响应头 Access-Control-Allow-Credentials: true, 否则浏览器会因为安全限制而报错, 拿不到响应;
axios 和 jQuery 在同域ajax请求时会带上cookie, 跨域请求不会, 跨域请求需要设置 withCredentials 和服务端响应头
# fetch 设置 credentials
使 fetch 带上 cookie:
fetch(url, {
credentials: "include", // include, same-origin, omit
})
- include: 跨域 ajax 带上 cookie
- same-origin: 仅同域 ajax 带上 cookie
- omit: 任何情况都不带 cookie
# axios 设置 withCredentials
使 axios 带上 cookie:
axios.get('http://server.com', { withCredentials: true})
# jQuery 设置 withCredentials
$.ajax({
method: 'get',
url: 'http://server.com',
xhrFields: {
withCredentials: true
}
})
参考:https://juejin.cn/post/6844903587764502536 https://juejin.cn/post/6844903648384778247
# 纯 js 怎么实现当你需要一个 js 的时候再去加载
第一种:函数引用,将所需加载方法放在匿名函数中传入
const laodScript = (url, callback) => {
// 创建一个 script
const script = document.createElement('script')
script.type = 'text/javascript'
if (script.readyState) {
// 判断当异步加载完后才执行回调 IE使用
script.onreadystatechange = () => {
if (script.readyState === 'complete' || script.readyState === 'loaded') {
callback()
}
}
} else {
// Chrome Safari Opera Firefox使用
script.onload = () => {
callback()
}
}
script.src = url
document.head.appendChild(script)
}
laodScript('./a.js', () => {
test()
})
第二种:使用 XMLHttpRequest 对象
const laodScript = (url, callback) => {
const xhr = new XMLHttpRequest()
xhr.open('get', url, true)
xhr.send()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.text = xhr.responseText
document.body.appendChild(script)
callback()
}
}
}
}
laodScript('./a.js', () => {
test()
})
第三种:Dynamic import 方式
// a.js
const test = () => {
console.log('test')
}
export default test
// 按需加载
import('./a.js', mod => {
mod.default()
})
# ts 的理解,优点和缺点
# 泛型和接口的区别
# 移动端适配方案有哪几种
# async await 和 promise 之间优缺点
# async/await 优点
- 它做到了真正的串行的同步写法,代码阅读相对容易
- 对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面
- 处理复杂流程时,在代码清晰度方面有优势
# async/await 缺点
- 无法处理 promise 返回的 reject 对象,要借助 try…catch…
- 用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
- try…catch…内部的变量无法传递给下一个 try…catch…,Promise 和 then/catch 内部定义的变量,能通过 then 链条的参数传递到下一个 then/catch,但是 async/await 的 try 内部的变量,如果用let和const定义则无法传递到下一个 try…catch…,只能在外层作用域先定义好。
# promise 的一些问题
- 一旦执行,无法中途取消,链式调用多个 then 中间不能随便跳出来
- 错误无法在外部被捕捉到,只能在内部进行预判处理,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
- Promise 内部如何执行,监测起来很难,当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
# es6 中新增内容,symbol、proxy、Object.is(===)、in、Object.assign
# Object.is 与 === 区别
严格相等检查和 Object.is() 之间的区别在于,如何处理 NaN 和如何处理负零 -0。
-0 === +0; // true
NaN === NaN; // false
Object.is(-0, +0) // false
Object.is(NaN, NaN) // true
# in 运算符
如果指定的属性在指定的对象或其原型链中,则in 运算符返回 true。
const car = { make: 'Honda', model: 'Accord', year: 1998 };
console.log('make' in car);
// expected output: true
delete car.make;
if ('make' in car === false) {
car.make = 'Suzuki';
}
console.log(car.make);
// expected output: "Suzuki"
- 语法
prop in object
prop: 一个字符串类型或者 symbol 类型的属性名或者数组索引(非symbol类型将会强制转为字符串)。 objectName: 检查它(或其原型链)是否包含具有指定名称的属性的对象。
# 如何去重
# 双重循环
实现 1:
Array.prototype.unique = function () {
const newArray = [];
let isRepeat;
for (let i = 0; i < this.length; i++) {
isRepeat = false;
for (let j = 0; j < newArray.length; j++) {
if (this[i] === newArray[j]) {
isRepeat = true;
break;
}
}
if (!isRepeat) {
newArray.push(this[i]);
}
}
return newArray;
}
实现 2:
Array.prototype.unique = function () {
const newArray = [];
let isRepeat;
for (let i = 0; i < this.length; i++) {
isRepeat = false;
for (let j = i + 1; j < this.length; j++) {
if (this[i] === this[j]) {
isRepeat = true;
break;
}
}
if (!isRepeat) {
newArray.push(this[i]);
}
}
return newArray;
}
# Array.prototype.indexOf()
基本思路:如果索引不是第一个索引,说明是重复值。
实现一:
- 利用 Array.prototype.filter() 过滤功能
- Array.prototype.indexOf() 返回的是第一个索引值
- 只将数组中元素第一次出现的返回
- 之后出现的将被过滤掉
const unique1 = item => {
return item.filter((i, index) => item.indexOf(i) === index)
}
# Array.prototype.sort()
基本思路:先对原数组进行排序,然后再进行元素比较。
const unique3 = item => {
const newArray = []
item.sort()
for (let i = 0; i < item.length; i++) {
if (item[i] !== item[i + 1]) {
newArray.push(item[i])
}
}
return newArray
}
# Array.prototype.includes()
const unique4 = item => {
const newArray = []
for (let i = 0; i < item.length; i++) {
if (!newArray.includes(item[i])) {
newArray.push(item[i])
}
}
return newArray
}
# Array.prototype.reduce()
onst unique = item => {
return item.sort().reduce((init, current) => {
console.log(init, current)
if (init.length === 0 || init[init.length - 1] !== current) {
init.push(current)
}
return init
}, [])
}
# Set
const unique4 = item => [...new Set(item)]
# Map
实现一:
const unique = item => {
const newArray = []
const tmp = new Map()
for (let i = 0; i < item.length; i++) {
if (!tmp.get(item[i])) {
tem.set(item[i], 1)
newArray.push(item[i])
}
}
return newArray
}
实现二:
const unique = item => {
const tmp = new Map()
return item.filter(i => {
return !tmp.has(i) && tmp.set(i, 1)
})
}
# map、for in、for of、filter 等等区别
# 深拷贝、浅拷贝、遇到循环引用如何处理
# 原型链、继承、作用域
# async/await 执行顺序
如下代码:
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')
},0)
async1()
new Promise(function(resolve){
console.log('promise1')
resolve()
}).then(function(){
console.log('promise2')
})
console.log('script end')
执行结果:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
- 首先执行整体代码,打印 script start
- 遇到 async1() 执行函数;打印 async1 start;然后执行 async2() 函数;打印 async2;await 后面的代码放到微任务执行(类似 promise then 里面的内容);
- 然后执行 new Promise,打印 promise1;
- 首次宏任务执行完毕,打印 script end;
- 然后执行微任务,从头开始,第一位微任务打印 async1 end,然后就是 Promise 里面的 promise2;微任务执行完毕;
- 执行宏任务;settimeout 中打印 setTimeout;执行完毕;
# 前端性能优化
https://juejin.cn/post/6981673766178783262#heading-7
# fetch api
# dom 操作如何优化
# 减少在循环内进行 DOM 操作,在循环外部进行 DOM 缓存
//优化前代码
function Loop() {
console.time('loop1')
for (var count = 0; count < 15000; count++) {
document.getElementById('text').innerHTML += 'dom'
}
console.timeEnd('loop1')
}
// 优化后代码
function Loop2() {
console.time('loop2')
var content = ''
for (var count = 0; count < 15000; count++) {
content += 'dom'
}
document.getElementById('text2').innerHTML += content
console.timeEnd('loop2')
}
优化前的代码中,每进行一次循环,都会读取一次 div 的 innerHtml 属性,并且对这个属性进行了重新赋值,即每循环一次就会操作两次DOM,因此执行时间很长,页面性能差。
在优化后的代码中,将要更新的 DOM 内容进行缓存,在循环时只操作字符串,循环结束后字符串的值写入到 div 中,只进行了一次查找innerHtml 属性和一次对该属性重新赋值的操作,因此同样的循环次数先,优化后的方法执行时间远远少于优化前。
# 操作 DOM 前,先把 DOM 节点删除或隐藏
const list1 = $('.list1')
list1.hide()
for (let i = 0; i < 15000; i++) {
const item = document.createElement('li')
item.append(document.createTextNode('0'))
list1.append(item)
}
list1.show()
display 属性值为 none 的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。
# 最小化重绘和重排
//优化前代码
const element = document.getElementById('mydiv')
element.style.height = '100px';
element.style.borderLeft = '1px'
element.style.padding = '20px'
在上面的代码中,每对 element 进行一次样式更改都会影响该元素的集合结构,最糟糕情况下会触发三次重排。 优化方式:利用 js 或 jquery 对该元素的 class 重新赋值,获得新的样式,这样减少了多次的 DOM 操作。
//优化后代码
// js 操作
.newStyle {
height: 100px;
border-left: 1px;
padding: 20px;
}
element.className = 'newStyle'
// jquery 操作
$(element).css({
height: 100px;
border-left: 1px;
padding: 20px;
})
# 减少访问
减少访问次数自然是想到缓存元素,但是要注意:
const ele = document.getElementById('ele')
这样并不是对 ele 进行缓存,每一次调用 ele 还是相当于访问了一次 id 为 ele 的节点。
# 改变选择器
获取元素最常见的有两种方法,getElementsByXXX() 和 queryselectorAll(),这两种选择器区别是很大的,前者是获取动态集合,后者是获取静态集合,举个例子。
// 假设一开始有 2 个 li
const lis = document.getElementsByTagName('li') // 动态集合
const ul = document.getElementsByTagName('ul')[0]
for(let i = 0; i < 3; i++) {
console.log(lis.length)
const newLi = document.createElement('li')
ul.appendChild(newLi)
}
// 输出结果:2, 3, 4
// 优化后
const lis = document.querySelectorAll('li') // 静态集合
const ul = document.getElementsByTagName('ul')[0]
for(let i = 0; i < 3; i++) {
console.log(lis.length)
const newLi = document.createElement('li')
ul.appendChild(newLi)
}
// 输出结果:2, 2, 2
对静态集合的操作不会引起对文档的重新查询,相比于动态集合更加优化。
# 事件委托
js 中的事件函数都是对象,如果事件函数过多会占用大量内存,而且绑定事件的 DOM 元素越多会增加访问 dom 的次数,对页面的交互就绪时间也会有延迟。所以诞生了事件委托,事件委托是利用了事件冒泡,只指定一个事件处理程序就可以管理某一类型的所有事件。
// 事件委托前
const lis = document.getElementsByTagName('li')
for(let i = 0; i < lis.length; i++) {
lis[i].onclick = function() {
console.log(this.innerHTML)
}
}
// 事件委托后
const ul = document.getElementsByTagName('ul')[0]
ul.onclick = function(event) {
console.log(event.target.innerHTML)
}
# DocumentFragment
createDocumentFragment() 方法是用了创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。它有一个很实用的特点,当请求把一个 DocumentFragment节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。 这个特性使得 DocumentFragment 成了占位符,暂时存放那些一次插入文档的节点
另外,当需要添加多个 dom 元素时,如果先将这些元素添加到 DocumentFragment 中,再统一将 DocumentFragment 添加到页面,会减少页面渲染 dom 的次数,效率会明显提升。 使用方法:
const frag = document.createDocumentFragment() // 创建一个 DOM 片段
for(let i = 0; i < 10000; i++) {
const li = document.createElement('li')
li.innerHTML = i
frag.appendChild(li) //将li元素加到文档碎片上
}
ul.appendChild(frag) //将文档碎片加到 ul 上
# 如何计算首屏时间以及优化
主要是通过 Window.performance 来实现计算,Web Performance API 允许网页访问某些函数来测量网页和 Web 应用程序的性能;
;(function() {
if (window.performance) {
const performance = window.performance;
if (performance) {
let t = performance.getEntriesByType('navigation')[0]
let r = 0
t || (r = (t = performance.timing).navigationStart)
const details = [{
key: "Redirect",
desc: "网页重定向的耗时",
value: t.redirectEnd - t.redirectStart
},
{
key: "AppCache",
desc: "检查本地缓存的耗时",
value: t.domainLookupStart - t.fetchStart
},
{
key: "DNS",
desc: "DNS查询的耗时",
value: t.domainLookupEnd - t.domainLookupStart
},
{
key: "TCP",
desc: "TCP连接的耗时",
value: t.connectEnd - t.connectStart
},
{
key: "Waiting(TTFB)",
desc: "从客户端发起请求到接收到响应的时间 / Time To First Byte",
value: t.responseStart - t.requestStart
},
{
key: "Content Download",
desc: "下载服务端返回数据的时间",
value: t.responseEnd - t.responseStart
},
{
key: "HTTP Total Time",
desc: "http请求总耗时",
value: t.responseEnd - t.requestStart
},
{
key: "DOMContentLoaded",
desc: "dom加载完成的时间",
value: t.domContentLoadedEventEnd - r
},
{
key: "Loaded",
desc: "页面load的总耗时",
value: t.loadEventEnd - r
},
{
key: "FirstContentful",
desc: "首屏时间",
value: performance.getEntriesByName("first-contentful-paint")[0].startTime - r
},
{
key: "Full",
desc: "白屏时间",
value: t.responseStart - r
}
];
console.log(details);
}
}
}())
# script 中 async 以及 defer
# 普通的 script 加载
例如 <link rel="preload">
。这些显式指定的预加载资源,也会被放入 memory cache 中。
前端性能优化的一条原则是将 script 标签放在 body 底部,为什么呢?因为 script 标签的加载和执行时会阻塞 DOM 结构渲染的,若是 script 标签放在头部,加载时间或者执行时间过长,会影响后续 DOM 的渲染,造成很长时间的页面白屏,前端体验会变得很差。 那么我们不妨亲自试一试?
为了使例子更加直观,直接用 nodejs 写一个服务。
// a.js
console.log(new Date(Date.now()), "我是外部a.js文件");
// b.js
console.log(new Date(Date.now()), "我是外部b.js文件");
//server.js
const fs = require("fs");
const http = require("http");
const a = fs.readFileSync("./a.js");
const b = fs.readFileSync("./b.js");
const serverBack = (req, res) => {
const url = req.url;
if (url === "/a.js") {
res.setHeader("Content-Type", "application/javascript; charset=UTF-8");
setTimeout(() => {
res.write(a);
res.end();
}, 9000);
return;
}
if (url === "/b.js") {
res.setHeader("Content-Type", "application/javascript; charset=UTF-8");
setTimeout(() => {
res.write(b);
res.end();
}, 5000);
return;
}
};
http.createServer(serverBack).listen(3003);
a.js 和 b.js 代码中都获取一下当前执行时间的时间戳,在 server.js 中读取两个文件的代码,若是访问相应的文件,返回即可,a.js 延迟 9s 返回,b.js 延迟 5s 返回,这样时间体验上会更清晰。 在前端的 html 文件中直接通过 script 标签引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>123213</div>
<script>
console.log(new Date(Date.now()), "内部script文件", "上");
</script>
<script src="http://localhost:3003/a.js"></script>
<script src="http://localhost:3003/b.js"></script>
<script>
console.log(new Date(Date.now()), "内部script文件", "下");
</script>
<div>
所有有脚本下面的内容
</div>
</body>
</html>
那么,直接运行这个 html 文件,控制台打印的最终结果为:
可以看出来,内部上边的 script 标签的最先被执行,然后大约 9s 后基本上是同时执行了外部引入的 a.js、b.js 和内部下边的 script 标签。执行内部最下边的 script 的几乎同时渲染出最下面的 dom 结构(可以看到 script 的加载会阻塞 dom 的渲染);那么它们三个的执行顺序是随机的吗?
当然不是!不管刷新多少次,它们的执行顺序是永远不会改变的,永远是 a.js->b.js-> 内部下边的 script 标签。我们在 server 端设置的 b.js 文件是 5s 返回结果,那么 b.js 文件肯定是先下载完的,可是会永远先执行 a.js 文件。
这是因为 <script>
标签是并发下载,同步执行的,也就是说,不管引入多少个外部 script 标签,它们都会异步去下载,但是下载完成后,会按照标签的顺序同步执行,这也是为什么 <script>
阻塞 DOM 渲染的原因。
那么外部 js 文件中是异步代码呢?
// a.js
console.log(new Date(Date.now()), "我是外部a.js文件");
setTimeout(() => {
console.log(new Date(Date.now()), "我是外部a.js文件", "setTimeout");
}, 6000);
// b.js
console.log(new Date(Date.now()), "我是外部b.js文件");
setTimeout(() => {
console.log(new Date(Date.now()), "我是外部b.js文件", "setTimeout");
}, 3000);
我们来看一下运行结果:
可以得出结论,a.js 和 b.js 文件依然会按照同步顺序执行,然后渲染出 dom;但是对于异步任务,也是遵循 eventloop 机制,将异步任务放入异步队列中去执行,内部 script 标签并不会等待异步任务执行完。
⚠️defer 和 async 属性只适用于外部引入的 js 文件
# defer 延迟脚本
现在我们来给外部的两个script标签加上defer属性:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>123213</div>
<script>
console.log(new Date(Date.now()), "内部script文件", "上");
</script>
<script src="http://localhost:3003/a.js" defer></script>
<script src="http://localhost:3003/b.js" defer></script>
<script>
console.log(new Date(Date.now()), "内部script文件", "下");
</script>
<div>
所有有脚本下面的内容
</div>
</body>
</html>
执行结果:
可以看出来,dom 是首先渲染出来的;内部的 script 标签中的代码并没有等 a.js 和 b.js 文件全部执行下载完就执行了,这也就是 defer 属性的延迟执行功能。虽然是延迟执行,但两个文件执行的先后顺序并不能改变。所以 defer 属性是并发下载,延迟同步执行,不会阻塞 DOM 渲染。 在《javascript 高级程序设计》中,原话是这样的: HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个执行。在现实中,延迟脚本并不一定会按照顺序执行,因此最好只包含一个延迟脚本。 本文中的浏览器是 Chrome,在 FirFox 和 Safari 中也尝试了一下,结果并没有发生改变。猜测作者是考虑到某些低版本浏览器没有做到严格的规范,才出此言。
# async 异步脚本
如下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>123213</div>
<script>
console.log(new Date(Date.now()), "内部script文件", "上");
</script>
<script src="http://localhost:3003/a.js" async></script>
<script src="http://localhost:3003/b.js" async></script>
<script>
console.log(new Date(Date.now()), "内部script文件", "下");
</script>
<div>
所有有脚本下面的内容
</div>
</body>
</html>
执行结果如下:
可以看出,内部下边的 script 标签中代码也没有等到 a.js 和 b.js 执行完就已经执行了。通过 b.js 的执行时间可以看出,b.js 文件是下载完立即执行的,并没有等待 a.js 执行完。所以 anync 属性是并发下载,异步执行,也不会阻塞 DOM 渲染。
# 总结
- script 标签加载都是并发加载,这是浏览器提供的功能。但是执行是按照标签写入顺序同步执行,异步代码遵循 eventloop。
- defer 属性是延迟同步执行,也就是等到 html 文档解析到
</html>
时才会回过头看 script 脚本是否全部下载完成?若是下载完成则按照顺序同步执行,否则继续等待所有标签下载完成在执行。 - async 属性是异步执行,也就是 script 脚本什么时候下载完,什么时候执行。
https://juejin.cn/post/6913156226008711182
# includes 与 indexOf
都是可以判断属性是否在数组中;区别
[NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1
# slice 与 splice 之前的区别
# slice
slice(start,end):方法可从已有数组中返回选定的元素,返回一个新数组,包含从 start 到 end(不包含该元素)的数组元素。
注意:该方法不会改变原数组,而是返回一个子数组,如果想删除数组中的一段元素,应该使用 Array.splice() 方法。
- start 参数:必须,规定从何处开始选取,如果为负数,规定从数组尾部算起的位置,-1 是指最后一个元素。
- end 参数:可选(如果该参数没有指定,那么切分的数组包含从 start 倒数组结束的所有元素,如果这个参数为负数,那么规定是从数组尾部开始算起的元素)。
var arr = [1,2,3,4,5];
console.log(arr.slice(1));//[2,3,4,5] 选择序列号从1到最后的所有元素组成的新数组。
console.log(arr.slice(1,3))//[2,3] 不包含end,序列号为3的元素
# splice
该方法向或者从数组中添加或者删除项目,返回被删除的项目。(该方法会改变原数组)
splice(index,howmany,item1,...itemX)
- inde 参数:必须,整数,规定添加或者删除的位置,使用负数,从数组尾部规定位置。
- howmany 参数:必须,要删除的数量,如果为 0,则不删除项目。
- tem1,...itemX 参数:可选,向数组添加的新项目。
var arr = [1,2,3,4,5];
console.log(arr.splice(2,1,"hello"));//[3] 返回的新数组
console.log(arr);//[1, 2, "hello", 4, 5] 改变了原数组
# 扩展运算符
# 使用扩展运算符可以快速扁平化二维数组
const arr = [1, [2, 3], [4, 5]]
const flatArr = [].concat(...arr)
console.log(flatArr) // [1, 2, 3, 4, 5]
使用递归,可以扁平化任意多维数组:
function flattenArray(arr) {
const flatArr = [].concat(...arr)
return flatArr.some(item => Array.isArray(item)) ? flattenArray(flatArr) : flatArr
}
const res = flattenArray([1, [2, 3, [4, 5, [6, 7]]]])
console.log(res) // [1, 2, 3, 4, 5, 6, 7]
# indexDB
IndexedDB 是一种使用浏览器存储大量数据的方法.它创造的数据可以被查询,并且可以离线使用. IndexedDB 对于那些需要存储大量数据,或者是需要离线使用的程序是非常有效的解决方法。
使用IndexedDB,你可以存储或者获取数据,使用一个 key 索引的。 你可以在事务(transaction)中完成对数据的修改。和大多数 web 存储解决方案相同,indexedDB 也遵从同源协议(same-origin policy). 所以你只能访问同域中存储的数据,而不能访问其他域的。
API 包含异步(asynchronous) API 和同步(synchronous)API两种。异步 API 适合大多数情况, 同步API必须同 WebWorkers 一同使用. 目前,没有主流浏览器支持同步 API。 即使同步 API 被支持了,你也会在大多数的情况使用异步 API。
# 创建一个 indexedDB 数据库
const request = indexedDB.open('myDatabase', 1)
request.addEventListener('success', e => {
console.log("连接数据库成功")
})
request.addEventListener('error', e => {
console.log("连接数据库失败")
})
# 创建一个对象仓库
const request = indexedDB.open('myDatabase', 2)
request.addEventListener('upgradeneeded', e => {
const db = e.target.result
const store = db.createObjectStore('Users', {keyPath: 'userId', autoIncrement: false})
console.log('创建对象仓库成功')
})
# 操作数据
// 添加数据
const request = indexedDB.open('myDatabase', 3)
request.addEventListener('success', e => {
const db = e.target.result
const tx = db.transaction('Users','readwrite')
const store = tx.objectStore('Users')
// 保存数据
const reqAdd = store.add({'userId': 1, 'userName': '李白', 'age': 24})
reqAdd.addEventListener('success', e => {
console.log('保存成功')
})
})
// 获取数据
const request = indexedDB.open('myDatabase', 3)
request.addEventListener('success', e => {
const db = e.target.result
const tx = db.transaction('Users','readwrite')
const store = tx.objectStore('Users')
// 获取数据
const reqGet = store.get(1)
reqGet.addEventListener('success', e => {
console.log(this.result.userName)// 李白
})
})
// 删除数据
const request = indexedDB.open('myDatabase', 3)
request.addEventListener('success', e => {
const db = e.target.result
const tx = db.transaction('Users','readwrite')
const store = tx.objectStore('Users')
// 删除数据
const reqDelete = store.delete(1)
reqDelete.addEventListener('success', e => {
console.log('删除数据成功')// 李白
})
})
# javaScript 空值合并操作符
只有当左侧为 null 和 undefined 时,才会返回右侧的数
- 空值合并操作符(??)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。
- 与逻辑或操作符(||)不同,逻辑或操作符会在左侧操作数为假值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,'' 或 0)时。见下面的例子。
let str = null || undefined
let result = str ?? '你真好看'
console.log(result) // 你真好看
const nullValue = null
const emptyText = "" // 空字符串,是一个假值,Boolean("") === false
const someNumber = 42
const valA = nullValue ?? 'valA 的默认值'
const valB = emptyText ?? 'valB 的默认值'
const valC = someNumber ?? 0
console.log(valA) // "valA 的默认值"
console.log(valB) // ""(空字符串虽然是假值,但不是 null 或者 undefined)
console.log(valC) // 42