高阶面试题
# 了解this嘛,bind,call,apply具体指什么
# this的指向
this永远指向最后调用它的那个对象
例 1:
var name = "windowsName";
function a() {
var name = "Cherry";
console.log(this.name); // windowsName
console.log("inner:" + this); // inner: Window
}
a();
console.log("outer:" + this) // outer: Window
2
3
4
5
6
7
8
9
10
前面没有调用的对象那么就是全局对象 window,这就相当于是 window.a()
;注意,这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined
,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined
。
注意:定时器、触发事件this指向window,即匿名函数(立即执行函数)的 this 永远指向 window
例 2:
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
window.a.fn();复制代码
2
3
4
5
6
7
8
这里打印 Cherry 的原因也是因为刚刚那句话“this 永远指向最后调用它的那个对象”,最后调用它的对象仍然是对象 a。
再来看一个比较坑的例子: 例 3:
var name = "windowsName";
var a = {
name : null,
// name: "Cherry",
fn : function () {
console.log(this.name); // windowsName
}
}
var f = a.fn;
f();复制代码
2
3
4
5
6
7
8
9
10
11
这里你可能会有疑问,为什么不是 Cherry
,这是因为虽然将 a 对象的 fn 方法赋值给变量 f 了,但是没有调用,再接着跟我念这一句话:“this 永远指向最后调用它的那个对象”,由于刚刚的 f 并没有调用,所以 fn()
最后仍然是被 window 调用的。所以 this 指向的也就是 window。
由以上五个例子我们可以看出,this 的指向并不是在创建的时候就可以确定的,在 es5 中,永远是this 永远指向最后调用它的那个对象。
再来看一个例子: 例 4:
var name = "windowsName";
function fn() {
var name = 'Cherry';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}
fn()复制代码
2
3
4
5
6
7
8
9
10
11
读到现在了应该能够理解这是为什么了吧(o゚▽゚)o。
# 怎么改变this的指向
- 使用ES6的箭头函数
- 在函数内部使用
_this = this
- 使用
apply
、call
、bind
- new实例化一个对象
# 箭头函数
箭头函数的this始终指向函数定义的this,而非执行时
# 使用apply、call、bind
如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。
如果第二个参数的值为null 或 undefined,则表示不需要传入任何参数。
apply 和 call 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.apply(a),100);
}
};
a.func2() // Cherry
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# JS中函数的调用
- 作为一个函数调用
- 函数作为方法调用
- 使用构造函数调用函数
- 作为函数方法调用函数
# new 命令的原理
使用new
命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的实例对象。
- 将这个空对象的原型,指向构造函数的
prototype
属性。 - 将这个空对象赋值给函数内部的
this
关键字。 - 开始执行构造函数内部的代码。(代码中this指向空对象(实例对象))
- 返回实例对象(或自定义对象)
# 匿名函数
匿名函数的 this 永远指向 window
匿名函数都是怎么定义的,首先,我们通常写的匿名函数都是自执行的,就是在匿名函数后面加 ()
让其自执行。其次就是虽然匿名函数不能被其他对象调用,但是可以被其他函数调用啊,比如 setTimeout
# NaN 是什么,typeof 会输出什么?
Not a Number,表示非数字,typeof NaN === 'number'
# JS 隐式转换,显示转换
一般非基础类型进行转换时会先调用 valueOf,如果 valueOf 无法返回基本类型值,就会调用 toString
# Promise原理
class MyPromise {
constructor(fn) {
this.resolvedCallbacks = [];
this.rejectedCallbacks = [];
this.state = 'PENDING';
this.value = '';
fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this.state === 'PENDING') {
this.state = 'RESOLVED';
this.value = value;
this.resolvedCallbacks.map(cb => cb(value));
}
}
reject(value) {
if (this.state === 'PENDING') {
this.state = 'REJECTED';
this.value = value;
this.rejectedCallbacks.map(cb => cb(value));
}
}
then(onFulfilled, onRejected) {
if (this.state === 'PENDING') {
this.resolvedCallbacks.push(onFulfilled);
this.rejectedCallbacks.push(onRejected);
}
if (this.state === 'RESOLVED') {
onFulfilled(this.value);
}
if (this.state === 'REJECTED') {
onRejected(this.value);
}
}
}
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
# setTimeout(fn, 0)
多久才执行,Event Loop
setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定
# 怎么加事件监听,两种
onclick 和 addEventListener
# 什么是原型链?
当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。这种通过 通过原型链接的逐级向上的查找链被称为原型链
# 什么是原型继承?
一个对象可以使用另外一个对象的属性或者方法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另外一个对象的属性和方法了。
# 说下对 JS 的了解吧
是基于原型的动态语言,主要独特特性有 this、原型和原型链。
JS 严格意义上来说分为:语言标准部分(ECMAScript)+ 宿主环境部分
# 语言标准部分
2015 年发布 ES6,引入诸多新特性使得能够编写大型项目变成可能,标准自 2015 之后以年号代号,每年一更
# 宿主环境部分
- 在浏览器宿主环境包括 DOM + BOM 等
- 在 Node,宿主环境包括一些文件、数据库、网络、与操作系统的交互等
# 函数中的arguments是数组吗?
是类数组,是属于鸭子类型的范畴,长得像数组
# 类数组转数组的方法了解一下?
- ... 运算符
- Array.from
- Array.prototype.slice.apply(arguments)
# 用过 TypeScript 吗?它的作用是什么?
为 JS 添加类型支持,以及提供最新版的 ES 语法的支持,是的利于团队协作和排错,开发大型项目
# 如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?
不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象
# 知道 ES6 的 Class 嘛?Static 关键字有了解嘛
为这个类的函数对象直接添加方法,而不是加在这个函数对象的原型对象上
# 事件循环机制 (Event Loop)
事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
先执行宏任务队列,然后执行微任务队列,然后开始下一轮事件循环,继续先执行宏任务队列,再执行微任务队列。
宏任务:script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
微任务:process.nextTick()/Promise
上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务
# 优先级
- setTimeout = setInterval 一个队列
- setTimeout > setImmediate
- process.nextTick > Promise
总的结论就是,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环
注意:只有一个任务队列的情况下,一直会保持先进先出的原则执行,但这时候我突然有一个任务优先级很高,需要尽快能够得到执行,所以就设计了微任务队列,就有了微任务和宏任务之分
# 手写扁平化
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result = result.concat(arr[i]);
}
}
return result;
}
const a = [1, [2, [3, 4]]];
console.log(flatten(a));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 手写题:实现柯里化
预先设置一些参数
柯里化是什么:是指这样一个函数,它接收函数 A,并且能返回一个新的函数,这个新的函数能够处理函数 A 的剩余参数
function createCurry(func, args) {
var argity = func.length;
var args = args || [];
return function () {
var _args = [].slice.apply(arguments);
args.push(..._args);
if (args.length < argity) {
return createCurry.call(this, func, args);
}
return func.apply(this, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 手写题:数组去重
- Array.form(new Set([1,2,2,2,2]))
- [...new Set([1,2,2,2])]
# DOM事件流
事件流描述的是从页面中接收事件的顺序。
事件发生时会在元素节点之间按照特定的顺序传播,这个传播过程即DOM事件流。
包括三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
我们知道,在dom模型中,html是多层次的,当一个html元素上产生事件时,该事件会在dom树元素节点之间按照特定的顺序去传播。传播路径的每一个节点,都会收到这个事件,这就是dom事件流。当事件发生后,就会从内向外逐级传播,因为事件流本身没有处理事件的能力,所以,处理事件的函数并不会绑定在该事件源上。例如我们点击了一个按钮,产生了一个click事件,click事件就会开始向上传播,一直到到处理这个事件的代码中。
事件捕获
:事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。
事件目标
:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
事件冒泡
:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发,当一个元素接收到事件的时候会把他接收到的事件传给自己的父级,一直到window 。
注意:
- JS代码只能执行捕获或者冒泡其中的一个阶段
- onclick 和 attachEvent 只能得到冒泡阶段
- addEventListener (type, listener[, useCapture]) 第三个参数如果是true,表示在事件捕获阶段调用事件处理程序;如果是false(不写默认就是false),表示在事件
冒泡阶段
电泳事件处理程序。 - 在实际开发中,我们很少使用事件捕获(低版本ie不兼容),我们更关注事件冒泡
- 有些事件是没有冒泡的,比如
onblur、onfocus、onmouseover、onmouseleave
- 虽然事件冒泡有时候会带来麻烦,但是有时候又会巧妙的做某些事情,我们后面讲解
- **e.preventDefaule()**该方法阻止默认事件(默认行为)标准 ,比如不让链接跳转
- **e.stopPropagation()**阻止冒泡,标准
事件按DOM事件流的顺序执行事件处理程序:
父级捕获
子级捕获
子级冒泡
父级冒泡
# 事件委托的原理(重)
事件委托的原理:不给每个子节点单独设置事件监听器,而是设置在其父节点上,然后利用冒泡原理设置每个子节点。
例如: 给 ul 注册点击事件,然后利用事件对象的 target 来找到当前点击的 li ,然后事件冒泡到 ul 上, ul 有注册事件,就会触发事件监听器。
# 事件委托的作用
只操作了一次 DOM ,提高了程序的性能。
# 为什么要事件委托?(重)
在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能
,因为需要不断的操作dom
,那么引起浏览器重绘和回流
的可能也就越多,页面交互的事件也就变的越长,这也就是为什么要减少dom操作的原因
。每一个事件处理函数,都是一个对象,那么多一个事件处理函数,内存中就会被多占用一部分空间。如果要用事件委托,就会将所有的操作放到js程序里面,只对它的父级(如果只有一个父级)这一个对象进行操作,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能
;
# 事件委托小案例
需求:鼠标放到li上,对应的li背景颜色变为灰色
<ul>
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
复制代码
2
3
4
5
6
7
普通实现
(给每个li都绑定一个事件让其变灰):
$("li").on("mouseover",function(){
$(this).css("background-color","gray").siblings().css("background-color","white");
})
复制代码
2
3
4
上面这种普通实现看似没有什么问题,但是如果在这段代码结束以后,我们动态的给ul又增加了一个li,那么新增的这个li是不带有事件的,如果有无数个li结点,我们的dom是吃不消的。
使用事件委托实现
js中事件是会冒泡的,所以this是可以变化的,但event.target不会变化
,它永远是直接接受事件的目标DOM元素
利用事件冒泡 只指定ul的事件处理 就可以控制ul下的所有的li的事件
$("ul").on("mouseover", function(e) {
$(e.target).css("background-color", "gray").siblings().css("background-color", "white");
})
复制代码
2
3
4
第一步:
给父元素绑定事件
给元素ul添加绑定事件,绑定mouseover事件设置css(也可通过addEventListener为点击事件click添加绑定)
第二步:
监听子元素的冒泡事件
这里默认是冒泡,点击子元素li会向上冒泡
第三步:
找到是哪个子元素的事件
通过匿名回调函数的参数e用来接收事件对象,通过target获取触发事件的目标(可以通过判断target的类型来确定是哪一类的子元素对象执行事件)
# http是如何实现长连接的,什么时候会超时
通过在头部(请求和响应头)设置 Connection: keep-alive,HTTP1.0协议支持,但是默认关闭,从HTTP1.1协议以后,连接默认都是长连接
HTTP头有Connection: Keep-alive
Keep-Alive: timeout=20,表示这个TCP通道可以保持20秒。另外还可能有max=XXX,表示这个长连接最多接收XXX次请求就断开。于客户端来说,如果服务器没有告诉客户端超时时间也没关系,服务端可能主动发起四次握手断开TCP连接,客户端能够知道该TCP连接已经无效;另外TCP还有心跳包来检测当前连接是否还活着,方法很多,避免浪费资源。
不希望使用长连接,则要在HTTP请求报文首部加上Connection: close
# 301与302的区别
# 1. 缓存
定义上已经给出,对于301请求,浏览器是默认给一个很长的缓存。而302是不缓存的。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
# 2. 搜索引擎
301: 旧地址A的资源不可访问了(永久移除), 重定向到网址B,搜索引擎会抓取网址B的内容,同时将网址保存为B网址。
302: 旧地址A的资源仍可访问,这个重定向只是临时从旧地址A跳转到B地址,这时搜索引擎会抓取B网址内容,但是会将网址保存为A的。
# 3. 安全
尽量使用301跳转,以防止网址劫持!
假如,A -> B。大部分的搜索引擎在大部分情况下,当收到302 重定向时,有的时候搜索引擎,尤其是Google,并不能总是抓取目标网址。比如说,有的时候A 网址很短,但是它做了一个302 重定向到B 网址,而B 网址是一个很长的乱七八糟的URL 网址,甚至还有可能包含一些问号之类的参数。很自然的,A 网址更加用户友好,而B 网址既难看,又不用户友好。这时Google 很有可能会仍然显示网址A。由于搜索引擎排名算法只是程序而不是人,在遇到302 重定向的时候,并不能像人一样的去准确判定哪一个网址更适当,这就造成了网址URL 劫持的可能性。也就是说,一个不道德的人在他自己的网址A 做一个302 重定向到你的网址B,出于某种原因, Google 搜索结果所显示的仍然是网址A,但是所用的网页内容却是你的网址B 上的内容,这种情况就叫做网址URL 劫持。你辛辛苦苦所写的内容就这样被别人偷走了。302 重定向所造成的网址URL 劫持现象,已经存在一段时间了。不过到目前为止,似乎也没有什么更好的解决方法。在正在进行的谷歌大爸爸数据中心转换中,302 重定向问题也是要被解决的目标之一。从一些搜索结果来看,网址劫持现象有所改善,但是并没有完全解决。
简单来说就是:有个坏人把他的电话来电转移到了一个明星那,让大家都以为他的电话是那个明星的。他的手机号成名后,就可以拉个微信群,大胆的假装明星,实现他的微shang梦,从此走上人生巅峰。
# 了解websocket嘛
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
对大部分web开发者来说,上面这段描述有点枯燥,其实只要记住几点:
- WebSocket可以在浏览器里使用
- 支持双向通信
- 使用很简单
# 连接保持+心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
- 发送方->接收方:ping
- 接收方->发送方:pong
# Sec-WebSocket-Key/Accept的作用
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept
在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用大致归纳如下:
- 避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)
- 确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)
- 用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)
- 可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。
- Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证。
# 你对 TCP 滑动窗口有了解嘛?
在 TCP 链接中,对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。而经常会存在发送端发送过多,而接收端无法消化的情况,所以就需要流量控制,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。而这种流量控制的过程就需要在发送端维护一个发送窗口,在接收端维持一个接收窗口。 TCP 滑动窗口分为两种: 发送窗口和接收窗口。
# TCP与UDP的区别
- UDP:无连接(不需要三次握手)、单播、多播的功能、不可靠性(想发就发,不会关心对方是否已经正确接收数据了)、没有拥塞控制、传输效率高(优点)
- TCP:面向连接、可靠(判断是否丢包)、拥塞控制
# 备注
- UDP适用于视频会议、电话、直播等
- TCP适用于文件传输
# 为什么 TCP 可靠?
TCP 的可靠性体现在有状态和控制
- 会精准记录那些数据发送了,那些数据被对方接收了,那些没有被接收,而且保证数据包按序到达,不允许半点差错,这就是有状态
- 当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发,这是可控制的
反之 UDP 就是无状态的和不可控制的
# DNS 缓存
为了让我们更快的拿到想要的 IP,DNS 广泛使用了缓存技术。DNS 缓存的原理非常简单,在一个 DNS 查询的过程中,当某一台 DNS 服务器接收到一个 DNS 应答(例如,包含某主机名到 IP 地址的映射)时,它就能够将映射缓存到本地,下次查询就可以直接用缓存里的内容。当然,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,一旦过了生存时间,这条记录就应该从缓存移出。
事实上,有了缓存,大多数 DNS 查询都绕过了根 DNS 服务器,需要向根 DNS 服务器发起查询的请求很少。
# 什么是https
HTTPS 是在 HTTP 和 TCP 之间建立了一个安全层,HTTP 与 TCP 通信的时候,必须先进过一个安全层,对数据包进行加密,然后将加密后的数据包传送给 TCP,相应的 TCP 必须将数据包解密,才能传给上面的 HTTP。 浏览器传输一个 client_random 和加密方法列表,服务器收到后,传给浏览器一个 server_random、加密方法列表和数字证书(包含了公钥),然后浏览器对数字证书进行合法验证,如果验证通过,则生成一个 pre_random,然后用公钥加密传给服务器,服务器用 client_random、server_random 和 pre_random ,使用公钥加密生成 secret,然后之后的传输使用这个 secret 作为秘钥来进行数据的加解密。
由于 HTTP 天生明文传输的特性,在 HTTP 的传输过程中,任何人都有可能从中截获、修改或者伪造请求发送,所以可以认为 HTTP 是不安全的。
在 HTTPS 中,使用
传输层安全性(TLS)
或安全套接字层(SSL)
对通信协议进行加密。也就是 HTTP + SSL(TLS) = HTTPS。
三个关键的指标
- 加密(Encryption)
- 数据一致性(Data integrity)
- 身份认证(Authentication)
# 认识 SSL/TLS
TLS(Transport Layer Security)
是 SSL(Secure Socket Layer)
的后续版本,它们是用于在互联网两台计算机之间用于身份验证
和加密
的一种协议。
TLS 在根本上使用对称加密
(加密和解密时使用的密钥都是同样的密钥)和 非对称加密
(公钥加密)两种形式。
# 什么是垃圾回收(GC)
垃圾回收算法
中比较常见的有 标记清除(Mark-Sweep)
和 引用计数(Reference Count)
,而Golang 采用 标记清除法
。并在 标记清除法
上使用 三色标记法
和 写屏障
等技术大大提高工作效率。
标记清除
收集器是 跟踪式
垃圾收集器,它的工作流程大致分为两个阶段:标记(Mark)
和 清除(Sweep)
:
- 标记阶段 — 从
根对象(root)
开始查找并标记堆
中所有存活对象
- 清除阶段 — 回收
堆中未被标记
的垃圾对象 并 将回收的内存加入空闲链表
三色标记算法将程序中的对象分成白色
、黑色
、灰色
三类。
- 白色:不确定对象
- 灰色:存活对象,子对象待处理
- 黑色:存活对象
# 并发时三色法存在的问题
由于 三色标记法
多了一个 白色状态
来存放 不确定对象
。 结合并发执行
的后续标记阶段,就有可能会造成一些遗漏:比如早先被标记为黑色对象
可能目前已经变不可达
。
但 并发情况下执行
依然存在问题:GC 过程中对象、指针被改变。
比如下面的例子:A (黑) -> B (灰) -> C (白) -> D (白)
通常 D 对象最后加入黑色集合不被回收。
但在并发情况下:A 获得了 D 的引用, 而C 对 D 的引用被用户删除 。 这是 GC并发运行,D 就在也没机会被标记为黑色(A 已经处理,这一轮不会再处理)
A (黑) -> B (灰) -> C (白)
↓
D (白)
复制代码
2
3
4
这显然是不允许的。
# 什么是写屏障
为了解决这个问题,Golang 的垃圾收集器使用了写屏障(Write Barrier)
技术:
当对象
新增
、更新
时,将对象着色为灰色
。
写屏障主要做一件事情:修改原来写逻辑,在对象新增
、更新
同时给它着色为灰色。 在并发情况下写屏障
保证三色标记法
的正确运行。
如此一来:即使 GC与用户程序并发执行。对象引用发生变化,垃圾收集器也能正确处理。
而写屏障标记成灰色的对象会在 在新的 GC 过程中所有已存对象 又从白色开始
逐步 按上面 三色法标记过程
分类处理
# 如何解决白屏问题
网络延迟,JS加载延迟 ,会阻塞页面
- 预渲染(骨架屏)
- 路由懒加载
- 减少http请求
- 图片懒加载
- 在create发请求,减少loading时间
- ssr渲染
- Gzip压缩
# 页面卡住了可能是因为什么
- 渲染不及时,页面掉帧
- 网页内存占用过高,运行卡顿
长时间占用js线程 页面回流和重绘较多 资源加载阻塞
内存泄漏导致内存过大
# PWA
PWA的中文名叫做渐进式网页应用,PWA
可以将 Web
和 App
各自的优势融合在一起:渐进式、可响应、可离线、实现类似 App
的交互、即时更新、安全、可以被搜索引擎检索、可推送、可安装、可链接。PWA
不是特指某一项技术,而是应用了多项技术的 Web App
。其核心技术包括 App Manifest
、Service Worker
、Web Push
,等等。
manifest.json
是一个简单的JSON文件,我们在 html
页面如下引用他:它描述了我们的图标在主屏幕上如何显示,以及图标点击进去的启动页是什么
# 图层
# 创建图层
- 拥有具有3D变换的css属性
- 拥有加速视频解码的video节点
- canvas节点
- css3动画的节点
- 拥有css加速属性的元素will-change:transform
# 重排
- 增加、删除、修改DOM
- 移动Dom
- 修改css样式的时候
- resize窗口变化的时候
- 修改网页的默认字体
- 获取某些属性(width、height)
- display:none会触发,而visibility:hidden触发重绘
# 优化
- transform来代替top和left等操作
- opacity+图层来代替visibility,即不触发重绘也不触发重排
- opacity即触发重绘,也触发重排(GPU底层设计如此)
- 不要使用table布局,三倍渲染速度
- DOM离线后在修改
- 可以先将元素设置为display:none在对其元素进行操作
- 利用文档碎片(documentFragment),vue提升性能的方式
- 动画实现过程中,启动GPU硬件加速:transform:translateZ(0)
- 为动画元素新建图层,提高元素的z-index
- 使用rAF编写动画
# 阻塞
# css阻塞
- style标签由html解析器解析,不阻塞浏览器渲染,也不阻塞DOM解析(可能闪屏)
- link引入的阻塞浏览器渲染,阻塞后面的js语句执行,不阻塞DOM解析(同步)
# 优化
- CDN外部资源加速
- 对css进行压缩
- 减少http请求
- 优化样式表代码
# js阻塞
- 阻塞DOM解析,渲染,原因:js可以给DOM设置样式
- 阻塞后续js的执行(同步)
# 备注
- css的解析和js的执行是互斥的,css解析的时候js停止执行,js执行的时候css停止解析
- 无论是css阻塞,还是js阻塞,都不会阻塞浏览器加载外部资源(图片、视频、样式、脚本),都会先发送请求获取资源
# 实现bigInt类型(大数相加)
// 实现bigInt类型
class BigInts{
constructor(number){
this.number = '' + number
}
add(addNumber){
let digist = addNumber.number + ''
let carry = 0
let len = this.number.length > digist.length ? this.number.length : digist.length
if(this.number.length>digist.length){
digist = digist.padStart(this.number.length,'0')
}else{
this.number = this.number.padStart(digist.length,'0')
}
let result = Array(len).fill(0)
console.log(result)
for(let i=len-1;i>=0;i--){
let sum = parseInt(digist[i]) + parseInt(this.number[i])
result[i] = sum%10 + carry
carry = Math.floor(sum/10)
}
result = result.join("")
return result
}
}
let a = new BigInts('234')
let b = new BigInts('12345678901234567890121111')
let sum = parseInt(a.add(b))
console.log(sum)
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
实现一个方法,输入是数据类型未知的value,有值返回true,无值返回false
eg:{} 为false
function type(value){
if(!value||value.length===0||value===null||Object.keys(value).length===0){
return false
}else{
return true
}
}
console.log(type({}))
2
3
4
5
6
7
8
# WebSocket与Ajax的区别
本质不同 Ajax 即异步 JavaScript 和 XML,是一种创建交互式网页的应用的网页开发技术 websocket 是 HTML5 的一种新协议,实现了浏览器和服务器的实时通信 生命周期不同:
- websocket 是长连接,会话一直保持
- ajax 发送接收之后就会断开
适用范围:
- websocket 用于前后端实时交互数据
- ajax 非实时
发起人:
- AJAX 客户端发起
- WebSocket 服务器端和客户端相互推送
了解 WebSocket 嘛? 长轮询和短轮询,WebSocket 是长轮询。 具体比如在一个电商场景,商品的库存可能会变化,所以需要及时反映给用户,所以客户端会不停的发请求,然后服务器端会不停的去查变化,不管变不变,都返回,这个是短轮询。 而长轮询则表现为如果没有变,就不返回,而是等待变或者超时(一般是十几秒)才返回,如果没有返回,客户端也不需要一直发请求,所以减少了双方的压力。
# HEAD请求方式
类似于get请求,只不过返回的响应中没有具体的内容
# fetch发送2次请求的原因
fetch 发送post 请求的时候,总是发送2 次,第一次状态码是204,第二次才成功?原因很简单,因为你用fetch 的post 请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。
# Cookie如何防范XSS攻击
XSS(跨站脚本攻击)是指攻击者在返回的HTML 中嵌入javascript 脚本,为了减轻这些 攻击,需要在HTTP 头部配上,set-cookie:
httponly-
这个属性可以防止XSS,它会禁止javascript 脚本来访问cookie。 secure -
这个属性告诉浏览器仅在请求为https 的时候发送cookie。 结果应该是这样的:Set-Cookie=<cookie-value>.....
# 设置一个元素的背景颜色,背景颜色会填充哪些区域
background-color 设置的背景颜色会填充元素的content、padding、border 区域。
# 字节码缓存
字节码缓存(Bytecode Cache)
,是浏览器性能优化机制中重要的一项,通过缓存 解析(pasing)+编译(compilation)的结果,减少网站的启动时间;当前市面上主流的浏览器都实现了字节码缓存功能;
# 跨页面通信
监听storage(本地存储)的变化
- key:修改或删除的key值,如果调用clear(),为null
- newVlue:新设置的值
- oldValue:调用改变钱的value值
- 触发该脚本变的文档url
- storageArea:当前的storage对象
使用方法:
window.addEventListener('storage',function(event){
//此处写具体业务逻辑
})
2
3
# localStorage数据存储为有效期
方法一:定时器法
class Storage{
constructor(){
this.timer = null
}
setItem(key,value,delay){
clearTimeout(this.timer)
localStorage.setItem(key,JSON.stringify(value))
this.timer = setTimeout(() => {
localStorage.removeItem(key)
}, delay);
}
getItem(key){
return JSON.parse(localStorage.getItem(key))
}
}
let person = new Storage()
person.setItem('username',{username:'小明',age:18},5000)
console.log(person.getItem('username'))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
方法二:时间戳法
class Storage{
constructor() {
}
// 设置存储
// key:需要存储的数据
// value:需要存储的数据
// expres:过期时间,毫秒单位
setItem(key,value,expires) {
let obj = {
value: value,
expires: expires,//有效时间
startTime:new Date().getTime()//记录存储数据的时间,转换为毫秒值存下来
}
// 判断是否设置了有效时间
if (obj.expires) {
// 如果设置了时间,把obj转换数据类型转换为字符串对象存起来
localStorage.setItem(key,JSON.stringify(obj))
}
// 如果没有设置有效时间,直接把value值存进去
else {
localStorage.setItem(key,JSON.stringify(obj.value))
}
}
// 获取存储数据
getItem(key) {
// 先定义一个变量临时存放提取的值
let temp = JSON.parse(localStorage.getItem(key))
// 判断有没有设置expires属性
// 如果有,就需要判断是否到期了
if (temp.expires) {
let data = new Date().getTime()
if (data - temp.startTime > temp.expires) {
// 此时说明数据已过期,清除掉
localStorage.removeItem(key)
// 直接return
return
}
else {
// 如果没有过期就输出
return temp.value
}
}
// 如果没有设置,直接输出
else {
return temp
}
}
}
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
# 0.2-0.1==0.1为true,0.8-0.6==0.2为fale
二进制只能精准表达2除尽的数字1/2, 1/4, 1/8
# clear的作用
指明该元素周围不可出现浮动元素
# ["1","2","3"].map(parseInt)=>[1,NaN,NaN]
["1","2","2","2","2","2"].map(parseInt) => [1, NaN, NaN, 2, 2, 2]
# border:none与0的区别
border:0表示边框宽度为0
当定义border:none时,表示无边框样式,浏览器并不会对边框进行渲染,也就没有实际的宽度;
定义边框时,除了设置宽度外,还必须设置边框的样式才能显示出来。
# 白屏时间first paint 和可交互时间dom ready的关系是?
先触发first paint,后触发dom ready
这里的dom ready指的是dom元素都已经被解析。只有dom树渲染完毕后,才会有元素,不然就会一片空白,而影响dom ready的因素也有很多,才会有那么多的性能化策略。
另一方面,在解析script时,dom加载会被阻塞。假如在dom加载之前出现alert提示框,在关闭提示框之前,页面都是一片空白。
# 浏览器的多线程
- GUI渲染线程
- JavaScript引擎线程
- 事件触发线程
- 定时器触发器线程
- 异步http请求线程
# WeakMap WeakSet
- WeakMap只接受对象作为键名
- WeakSet的成员只能是对象(不可重复值)
- 不计入垃圾回收机制
- 都没有遍历操作,没有size属性、clear方法
# 隐士转换
- 基本数据类型:String=>Number,Boolean=>Number
- 复杂数据类型:先调用valueOf()方法获取原始值,然后调用toString转为字符串
- 布尔值类型为false情况:0、-0、NaN、undefined、null、""、false、document.all() //可以判断浏览器是否是IE
- 引用类型值在做数学运算的时候,会先调用valueOf,比较不了再调用toString(),无数学运算先调用toString(),如console.log(
${obj}
)为toString的值,并且类型为String
const obj = {
valueOf:()=>1,
toString:()=>2
}
console.log(obj+2) //3
console.log(`${obj}`+2) //22
2
3
4
5
6
//关系运算符:将其他数据类型转换为数字
console.log([]==0) // true 隐士转换为空字符串
console.log(![]==0) // true !运算符大于关系运算符不存在转换
console.log([]==![]) //true 隐士转换
console.log([]==[]) //false 存储的是地址
//console.log({}.valueOf().toString()) [object Object]
//console.log([].valueOf().toString()) 空字符串
console.log({}==!{}) //false
console.log({}=={}) //false 存储的是地址
2
3
4
5
6
7
8
9
10
11
# JSONP
通过一些标签发出的请求则不会被进行同源检查,比如script标签,img标签等等,本文讲述JSONP便是通过script标签做的请求。
# 流程:
在发请求先,准备一个全局的接收函数
window.myCallback = (res)=>{ //声明一个全局函数 'callback',用于接收响应数据 console.log(res) }
1
2
3在html创建script标签,发出请求
<html> .... <script> window.myCallback = (res)=>{ //这里为上一步定义的全局函数 console.log(res) } </script> <script url="xxx?callback=myCallback"> //script标签的请求必须在写在定义全局函数之后 //这里需将全局函数的函数名作为参数callback的value传递 //这里callback这个键名是前后端约定好的 </script> </body> </html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14服务端接收到请求,将如下数据相应回
myCallback({ //一个函数的调用,将数据作为参数传递进去,再将整个函数的调用返回给客户端 name:'ahreal', age:18 })
1
2
3
4客户端接收到服务端的相应,相当于:
<html> .... <script> window.myCallback = (res)=>{ //这里为上一步定义的全局函数 console.log(res) } </script> <script> //将接收到的数据作为script标签里面的内容展开执行 myCallback({ name:'ahreal', age:18 }) </script> </body> </html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15控制台输出
# JSONP和AJAX请求的异同
# 相同点:
- 使用的目的一致,都是客户端向服务端请求数据,将数据拿回客户端进行处理。
# 不同点:
- ajax请求是一种官方推出的请求方式,通过xhr对象去实现,jsonp是民间发明,script标签实现的请求。
- ajax是一个异步请求,jsonp是一个同步请求
- ajax存在同源检查,jsonp不存在同源检查,后端无需做解决跨域的响应头。
- ajax支持各种请求的方式,而jsonp只支持get请求
- ajax的使用更加简便,而jsonp的使用较为麻烦。
# RIC
- 处理用户的交互
- JS 解析执行
- 帧开始。窗口尺寸变更,页面滚去等的处理
- requestAnimationFrame(rAF)
- 布局
- 绘制
# requestIdleCallback
上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback
里注册的任务。
requestIdleCallback:: 会在浏览器空闲时间执行回调,也就是允许开发人员在主事件循环中执行低优先级任务,而不影响一些延迟关键事件。如果有多个回调,会按照先进先出原则执行,但是当传入了 timeout,为了避免超时,有可能会打乱这个顺序。
# ES5/ES6 的继承除了写法以外还有什么区别?
class和let和const一样存在暂时性死区,没有函数提升
let dog = new Dog(); // Uncaught ReferenceError: Cannot access 'Dog' before initialization
class Dog {
}
2
3
class声明内部会启用严格模式
function Animal() {
this.name = 'le';
age = 10;
};
const ani = new Animal();
class Dog {
constructor() {
sex = 1; // Uncaught ReferenceError: sex is not defined
}
}
let dog = new Dog();
2
3
4
5
6
7
8
9
10
11
12
必须使用new来调用class
function Animal() {
this.name = 'le';
console.log('animal');
};
const ani = Animal();
class Dog {
constructor() {
this.name = 'le';
}
}
let dog = Dog(); // Uncaught TypeError: Class constructor Dog cannot be invoked without 'new'
2
3
4
5
6
7
8
9
10
11
12
class的静态方法和原型方法都是不可枚举的
function Animal() {
this.name = 'le';
};
Animal.static_run = function() {
console.log('animal static run');
}
Animal.prototype.say = function() {
console.log('animal say');
}
Animal.prototype.eat = function() {
console.log('animal eat');
}
const ani = new Animal();
class Dog {
constructor() {
this.name = 'le';
}
static static_run() {
console.log('dog static run');
}
say() {
console.log('dog say');
}
eat() {
console.log('dog eat');
}
}
let dog = new Dog();
console.log('Animal 静态方法:', Object.keys(Animal)); // Animal 静态方法: ["static_run"]
console.log('Animal 原型方法:', Object.keys(Animal.prototype)); // Animal 原型方法: (2) ["say", "eat"]
console.log('Dog 静态方法:', Object.keys(Dog)); // Dog 静态方法: []
console.log('Dog 原型方法:', Object.keys(Dog.prototype)); //Dog 原型方法: []
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
class的静态方法和原型方法都没有prototype,不能使用new来调用
function Animal() {
this.name = 'le';
};
Animal.static_run = function() {
console.log('animal static run');
}
Animal.prototype.say = function() {
console.log('animal say');
}
Animal.prototype.eat = function() {
console.log('animal eat');
}
const ani = new Animal();
class Dog {
constructor() {
this.name = 'le';
}
static static_run() {
console.log('dog static run');
}
say() {
console.log('dog say');
}
eat() {
console.log('dog eat');
}
}
let dog = new Dog();
console.log('Animal 静态方法static_run属性:', Reflect.ownKeys(Animal.static_run));
console.log('Animal 原型方法say属性:', Reflect.ownKeys(Animal.prototype.say));
console.log('Dog 静态方法static_run属性:', Reflect.ownKeys(Dog.static_run));
console.log('Dog 原型方法say属性:', Reflect.ownKeys(Dog.prototype.say));
/**
* Animal 静态方法static_run属性: (5) ["length", "name", "arguments", "caller", "prototype"]
* index.html:81 Animal 原型方法say属性: (5) ["length", "name", "arguments", "caller", "prototype"]
* index.html:82 Dog 静态方法static_run属性: (2) ["length", "name"]
* index.html:83 Dog 原型方法say属性: (2) ["length", "name"]
*/
// const obj_a_static_run = new Animal.static_run(); // animal static run
// const obj_a_say = new Animal.prototype.say(); // animal say
// const obj_static_run = new Dog.static_run(); // Uncaught TypeError: Dog.static_run is not a constructor
const obj_say = new Dog.prototype.say(); // Uncaught TypeError: Dog.prototype.say is not a constructor
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
class 子类的proto等于父类,es5中子类的proto为Function.prototype
function People() {
this.name = 'le';
};
function Chinese() {
People.call(this);
}
Chinese.prototype = Object.create(People.prototype);
Chinese.prototype.constructor = Chinese;
class Animal {
constructor() {
this.name = 'le';
}
}
class Dog extends Animal {};
console.log(Chinese.__proto__ === People); // false
console.log(Chinese.__proto__ === Function.prototype); // true
console.log(Dog.__proto__ === Animal); // true
console.log(Animal.__proto__ === Function.prototype); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ES5继承是先新建子类的实例对象this,再将父类的属性添加到子类上,ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承
function ES5Array(len) {
Array.call(this, len); // 这里没有生效,无法进入Array的构造函数
}
ES5Array.prototype = Object.create(Array.prototype);
ES5Array.prototype.constructor = ES5Array;
class ES6Array extends Array {
constructor(len) {
super(len);
}
};
// 或者 class ES6Array extends Array{}
const arr5 = new ES5Array(3);
const arr6 = new ES6Array(3);
console.log(arr5.length); // 0
console.log(arr6.length); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# AOP
- 侧面(也就是切面) 用来描述分散在对象、类或函数中的横切关注点。 重点在这,分散在对象中的横切关注点,可以猜一下是什么,应该就是不同对象之间公用的部分
- 简而言之,AOP是针对业务处理过程中的切面(即非业务逻辑部分,例如错误处理,埋点,日志等)进行提取.
# 不同域名之间共享localStorage/sessionStorage
# 问题
两个不同的域名的localStorage不能直接互相访问。那么如何在aaa.com
中如何调用bbb.com
的localStorage?
# 实现原理
1.在aaa.com
的页面中,在页面中嵌入一个src为bbb.com
的iframe
,此时这个iframe
里可以调用bbb.com
的localStorage。
2.用postMessage
方法实现页面与iframe
之间的通信。
综合1、2便可以实现aaa.com
中调用bbb.com
的localStorage。
# 优化iframe
我们可以在bbb.com
中写一个专门负责共享localStorage的页面,例如叫做page1.html
,这样可以防止无用的资源加载到iframe
中。
# 示例
以在aaa.com
中读取bbb.com
中的localStorage的item1
为例,写同理:
bbb.com
中page1.html
,监听aaa.com
通过postMessage
传来的信息,读取localStorage,然后再使用postMessage
方法传给aaa.com
的接收者。
<!DOCTYPE html>
<html lang="en-US">
<head>
<script type="text/javascript">
window.addEventListener('message', function(event) {
if (event.origin === 'https://aaa.com') {
const { key } = event.data;
const value = localStorage.getItem(key);
event.source.postMessage({wallets: wallets}, event.origin);
}
}, false);
</script>
</head>
<body>
This page is for sharing localstorage.
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在aaa.com
的页面中加入一个src为bbb.com/page1.html
隐藏的iframe
。
<iframe id="bbb-iframe" src="https://bbb.com/page1.html" style="display:none;"></iframe>
在aaa.com
的页面中加入下面script标签。在页面加载完毕时通过postMessage
告诉iframe
中监听者,读取item1
。监听bbb.com
传回的item1
的值并输出。
<script type="text/javascript">
window.onload = function() {
const bbbIframe = document.getElementById('bbb-iframe');
bbbIframe.contentWindow.postMessage({key: 'item1'}, 'https://bbb.com');
}
window.addEventListener('message', function(event) {
if (event.origin === 'https://bbb.com') {
console.log(event.data);
}
}, false);
</script>
2
3
4
5
6
7
8
9
10
11
# 伪类与伪元素的区别
- 伪类用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。比如说,当用户悬停在指定的元素时,我们可以通过:hover 来描述这个元素的状态。虽然它和普通的 css 类相似,可以为已有的元素添加样式,但是它只有处于 dom 树无法描述的状态下才能为元素添加样式,所以将其称为伪类。
- 伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。
注意:CSS3 标准要求伪元素使用双冒号的写法,但也依然支持单冒号的写法。为了向后兼容,我们建议你在目前还是使用单冒号的写法。
实际上,伪元素使用单冒号还是双冒号很难说得清谁对谁错,你可以按照个人的喜好来选择某一种写法。
# delete关键字
用来删除一个对象的属性
删除对象不存在的属性,delete无效,但返回true
可以删除隐式全局变量,但不可已删除显示全局变量。全局变量其实是global对象(window)的属性
x = 10
var y = 20
delete x //true;
delete y //false
2
3
4
5
内置对象的内置属性不能被删除,用户自定义的属性可以被删除
obj = {
h : 10
}
var obj1 = {
h: 10
}
delete Math.PI; // false
delte obj.h; //true
delete obj; //ture ,obj 是全局变量的属性,而不是变量。
delete obj1.h;//true
delete obj1; //false 全局显示变量不能被删除
function fn(){
var z = 10;
delete z; //false
//z是局部变量,不能被删除,delete只能删除对象的属性。
}
delete fn; //false
//fn 相当于是一个匿名变量,所以也不能被删除。
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
不能删除一个对象从原型继承而来的属性,但是可以直接从原型上删掉它
function foo(){}
foo.prototype.name = 'zhangsan';
var f = new foo();
//delete只能删除自己的属性,不能删除继承来的属性
delete f.name; // false
console.log(f.name);//zhangsan
delete foo.prototype.anme; // true
console.log(f.name); // undefined
2
3
4
5
6
7
8
9
10
11
12
13
delete删除数组某项元素时,被删除的元素会从该数组中删除,并且索引没有了,但数组的length并不会改变
var arr = [1,3,4,6,73,2];
delete arr[2];
console.log(arr.length); // 6
console.log(arr[2]); //undefiend
consoel.log(arr); //[ 1, 3, , 6, 73, 2 ]
2
3
4
5
6
但是这里存在一个问题
console.log(1 in arr) // false
所以如果想把数组中某一项赋值成undefined时,不应该使用delete操作符,而是直接使用下边赋值
arr[1] = undefined;
// 这样就可以解决上面的问题
console.log(1 in arr) // true
2
3
在forEach 循环中删除元素,不会影响循环结果
var arr = [1,3,5,21,3,4,53,21,5,2];
arr.forEach(function(val,index){
if(val < 10){
delete arr[index];
}
})
console.log(arr); //[ , , , 21, , , 53, 21, , ]
//可以使用filter过滤掉空值 [ 21, 53, 21 ]
arr = arr.filter(function(val){return val});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# in关键字
当‘对象’是数组时:“变量”指的是数组的“索引”;
当‘对象’为对象是,“变量”指的是对象的“属性”。
let arrs = ['a','b','2','3']
console.log('b' in arrs) //false
console.log(4 in arrs) //true
let obj1 = {
a:'one',
b:'two',
c:'three'
}
console.log(2 in obj1) //false
console.log('b' in obj1) //true
2
3
4
5
6
7
8
9
10
11
# 手写红绿灯
一个运用async/await实现红绿灯
function sleep(duration){
return new Promise(resolve => {
setTimeout(resolve,duration)
})
}
async function changeColor(duration,color){
document.getElementById('circle').style.background = color
await sleep(duration)
console.log(color)
}
async function main(){
while(true){
await changeColor(3000,'green')
await changeColor(1000,'yellow')
await changeColor(2000,'red')
}
}
main()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 有利于SEO
搜索引擎是识别文字,而不识别图片的,“爬虫”没有办法读取 JavaScript ,同时也要少用 iframe ,因为“爬虫”一般不会去读取它里面的内容。
- meta中的name属性网页优化3剑客,title、description、keywords
- logo用a链接,样式用背景图片代替
- 使用语义化标签,不应该滥用b和i,用em和strong
- 利用img的
alt
属性 - a链接的
rel="nofollow"
忽略跟踪:因为“爬虫”分配到每个页面的权重是一定的,为了集中网页权重并将权重分给其他必要的链接 - 提高加载速度:我们应尽量让结构(HTML)、表现(CSS)及行为(JavaScript)三者分离。此外,如果
不为 <img> 定义宽高
,那么会引起页面重新渲染,同样也会影响加载速度
。一旦加载超时,“爬虫”就会放弃爬取。 - 扁平化网站结构:一般来说,
一个网站的结构层次越少,越有利于“爬虫”的爬取。
所以目录结构一般不多于 3 级,否则“爬虫”很容易不愿意继续往下爬。就像用户在操作一个网页一样,层级大于 3 就很影响用户体验了,“爬虫”就是模仿用户的心理。 - 合理安排重要内容的位置:我们应该将
含重要内容的 HTML 代码放在最前面
,因为“爬虫”爬取 HTML 的顺序是从上到下
,有的搜索引擎对抓取长度有限制
,所以要保证重要内容被优先爬取
。
注意:
<title>
、meta description
和meta
keywords
三者的权重依次减小,我们要想网页有好的排名,必须合理使用这三个标签。
# href与src的区别
- 用来建立当前元素和文档之间的链接。常用的有:link、a
- 在请求 src 资源时会将其指向的资源下载并应用到文档中,常用的有script,img 、iframe;
(1)若在文档中添加href ,浏览器会识别该文档为 CSS 文件,就会并行下载资源并且不会停止对当前文档的处理。这也是为什么建议使用 link 方式加载 CSS,而不是使用 @import 方式。
(2)当浏览器解析到src ,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等也如此,类似于将所指向资源应用到当前内容。这也是为什么建议把 js 脚本放在底部而不是头部的原因。