多次bind绑定
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a) 等同于 // fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2() 因此:不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 windowthis的优先级
要注意:对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
==的类型转换问题
闭包问题
闭包存在的意义就是让我们可以间接访问函数内部的变量
在实际项目中 可以用来隐藏数据 只对外提供API
// 闭包隐藏数据,只提供API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
const c = createCache()
c.set('a', 100)
console.log( c.get('a') )
//无法直接访问 data[a] 浅拷贝和深拷贝
浅拷贝:
| Object.assign | 如果value是对象的话 只能拷贝地址 let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1 |
| 运算符 ... | let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1 |
| JSON.parse(JSON.stringify(object)) | let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
|
| 手写深拷贝 | function deepClone(obj){
if(typeof obj !=='object'||obj == null){
return obj
}
let res;
if(Array.isArray(obj)){
res = [];
}else{
res = {};
}
for (let key in obj){
if (obj.hasOwnProperty(key)){
res[key] = deepClone(obj[key])
}
}
return res;
} |
函数提升和变量提升
要注意 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
ES6模块化和Nodejs模块化
模块化的好处:
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
| Nodejs (commonjs) | demo: // a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1 在浏览器中会出现堵塞情况 |
| ES module | 结合了commonjs和AMD的优点 也就是一直在用的import/export export function test (args) {
// body...
console.log(args);
}
// 默认导出模块,一个文件中只能定义一个
export default function() {...};
export const name = "lyn";
// _代表引入的export default的内容
import { test, name } from './a.js';
test(`my name is ${name}`); es6输出的是值的引用并且是静态引入 编译时就引入了 |
- CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
- CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- ES Module 会编译成 require/exports来执行的
手写一个promise 重点
// 三个常量用于表示状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
function MyPromise(fn) {
const that = this
this.state = PENDING
// value 变量用于保存 resolve 或者 reject 中传入的值
this.value = null
// 用于保存 then 中的回调
//因为当执行完 Promise 时状态可能还是等待中
//这时候应该把 then 中的回调保存起来用于状态改变时使用
that.resolvedCallbacks = []
that.rejectedCallbacks = []
function resolve(value) {
// 首先两个函数都得判断当前状态是否为等待中
if(that.state === PENDING) {
that.state = RESOLVED
that.value = value
// 遍历回调数组并执行
that.resolvedCallbacks.map(cb=>cb(that.value))
}
}
function reject(value) {
if(that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb=>cb(that.value))
}
}
// 完成以上两个函数以后,我们就该实现如何执行 Promise 中传入的函数了
try {
fn(resolve,reject)
}cach(e){
reject(e)
}
}
// 最后我们来实现较为复杂的 then 函数
MyPromise.prototype.then = function(onFulfilled,onRejected){
const that = this
// 判断两个参数是否为函数类型,因为这两个参数是可选参数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v=>v
onRejected = typeof onRejected === 'function' ? onRejected : e=>throw e
// 当状态不是等待态时,就去执行相对应的函数。
//如果状态是等待态的话,就往回调函数中 push 函数
if(this.state === PENDING) {
this.resolvedCallbacks.push(onFulfilled)
this.rejectedCallbacks.push(onRejected)
}
if(this.state === RESOLVED) {
onFulfilled(that.value)
}
if(this.state === REJECTED) {
onRejected(that.value)
}
}
进程和线程
放在应用上来说,进程就代表了一个程序。线程是进程中的更小单位,描述了执行一段指令所需的时间
如果以浏览器为例:
如果以浏览器为例:
当你打开一个 Tab 页时,其实就是创建了一个进程
一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。
当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁
注意:
如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中
Node中的event loop
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用(可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on('close', ...)
└───────────────────────┘ nodejs与浏览器相比,最大的区别就是 nodejs的 MacroTask 分好几种,而这好几种又有不同的 task queue,而不同的 task queue 又有顺序区别,而 MicroTask 是穿插在每一种【注意不是每一个!】MacroTask 之间的。
如图中所示:
setTimeout/setInterval 属于 timers 类型;
setImmediate 属于 check 类型;
socket 的 close 事件属于 close callbacks 类型;
其他 MacroTask 都属于 poll 类型。
process.nextTick 本质上属于 MicroTask,但是它先于所有其他 MicroTask 执行;
所有 MicroTask 的执行时机,是不同类型 MacroTask 切换的时候。
idle/prepare 仅供内部调用,我们可以忽略。
pending callbacks 不太常见,我们也可以忽略。
setImmediate 属于 check 类型;
socket 的 close 事件属于 close callbacks 类型;
其他 MacroTask 都属于 poll 类型。
process.nextTick 本质上属于 MicroTask,但是它先于所有其他 MicroTask 执行;
所有 MicroTask 的执行时机,是不同类型 MacroTask 切换的时候。
idle/prepare 仅供内部调用,我们可以忽略。
pending callbacks 不太常见,我们也可以忽略。
总的来说 nodejs的event loop可以总结为:
- 先执行所有类型为 timers 的 MacroTask,然后执行所有的 MicroTask(注意 NextTick 要优先哦);
- 进入 poll 阶段,执行几乎所有 MacroTask,然后执行所有的 MicroTask;
- 再执行所有类型为 check 的 MacroTask,然后执行所有的 MicroTask;
- 再执行所有类型为 close callbacks 的 MacroTask,然后执行所有的 MicroTask;
- 至此,完成一个 Tick,回到 timers 阶段;
- ……
- 如此反复,无穷无尽……
| setTimeout 与 setImmediate 的顺序 | Node 对 timers 的过期检查不一定靠谱 因此不能在预定时间立刻执行 这导致setTimeout 与 setImmediate 的顺序不确定 如: setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
}) 此时,虽然 setTimeout 延时为 0 但是一般情况 Node 把 0 会设置为 1ms 所以,当 Node 准备 event loop 的时间大于 1ms 时: 进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout; 反之,若进入 timers 阶段用时小于 1ms: setTimeout 尚未到期,则会错过 timers 阶段, 先进入 check 阶段,执行 setImmediate |
| poll阶段 | poll有两个功能:
|
| pending callbacks阶段 | 有时候也被叫做 I/O callbacks 它所执行的回调是比较特殊的、且不需要关心的 严格来说: i/o callbacks并不是处理文件i/o的callback 而是处理一些系统调用错误, 比如网络 stream, pipe, tcp, udp通信的错误callback |
手写call bind apply
call
Function.prototype.myCall(context,...args){
if(this === Function.prototype ){
return undefined;
}
context = context ||window;
const fn = Symbol();
context[fn]=this;
const res = context[fn](...args);
delete context[fn];
return res;
} apply Function.prototype.myCall(context,args){
if(this === Function.prototype ){
return undefined;
}
context = context ||window;
const fn = Symbol();
context[fn]=this;
let res;
if(Array.isArray(args)){
res = context[fn](args);
}else{
res = context[fn](...args);
}
delete context[fn];
return res;
} bind Function.prototype.myBind = function (context,...args){
if(this === Function.prototype){
throw new TypeError('Error');
}
context = context ||window;
const self = this;
return function () {
return self.bind(context, ...args)
}
} 手写instanceof
function myInstanceof(target,origin){
const proto = target.__proto__;
if(proto){
if(proto === origin.prototype){
return true;
}else{
return myInstanceof(proto,origin)
}
}else{
return false;
}
} 0.1+0.2=0.3
计算机是通过二进制来存储东西的,所以 0.1 在二进制中会表示为0.1 = 2^-4 * 1.10011(0011)
也就是说,十进制小数的二进制表示是无限循环的
但JS采用的浮点数标准是IEEE 754双精度版本(64位) 会裁剪数字 导致了精度丢失
跨域
跨域
| JSONP | 可能有多个JSONP使用同一个回调函数 这时候可以做一下封装 function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallback] = function(data) {
success && success(data)
}
document.head.appendChild(script);
}
jsonp('http://xxx', 'callback', function(value) {
console.log(value)
}) JSONP 使用简单且兼容性不错,但是只限于 get 请求 |
| CORS | 后端 |
| document.domain |
|
| postMessage | 这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息 |
cookie问题
| 属性 | 作用 |
|---|---|
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |
浏览器缓存机制
从三个角度来说,分别是:缓存位置 缓存策略 实际场景应用缓存策略
1 缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络
| Service Worker | 是运行在浏览器背后的独立线程 使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全 可以自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。 如果Service Worker没有命中的话,会根据优先级去查找数据 但是:不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。 |
| Memory Cache | 就是内存中的缓存 读取比磁盘快 但是时效性短 一旦关闭 Tab 页面,内存中的缓存也就被释放了 |
| Disk Cache | 是存储在硬盘中的缓存,读取速度慢点 但是容量大 时效性长 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。 它会根据 ·HTTP Herder· 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用, 哪些资源已经过期需要重新请求。 并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据 |
| Push Cache | 在以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。 在其中的缓存只能被使用一次 |
常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的
| 强制缓存 | 强制缓存可以通过设置两种HTTP HEADER来实现: Expires 和 Cache-Control 其中 Expiries 表示资源会在 (某一具体时间) 后过期 Expiries受限于本地时间,如果修改了本地时间,可能会造成缓存失效。 而Cache-Control优先级更高 表示资源会在某一时间段后过期 Cache-Control的值有:
|
| 协商缓存 | 是一个服务端缓存策略:由服务端判断可不可以使用缓存资源,但不是在服务端做缓存 说白了,就是用服务器判断客户端资源是否和服务端一样 如果一样,则返回304 如果不一样 就返回200 (根据资源标识,来判断两边的资源是否一样) 资源标识也在resoponse headers中
|
而Last-Modified则是最后修改时间,以秒计时,如果在不可感知的时间内修改了文件,则服务端仍不会改变Last-Modified
3.实际场景应用缓存策略
| 频繁变动的资源 | 对于频繁变动的资源 首先 或者 Last-Modified 来验证资源是否有效。 这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。 |
| 代码文件 | 一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理, 只有当代码修改后才会生成新的文件名。 基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000, 这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件 否则就一直使用缓存 |
浏览器渲染过程
| 生成DOM树 | 网络中传输的内容都是 0 1这些字节数据 所以浏览器必须这些字节数据转换为字符串 也就是我们写的代码 其中Token:标记 还是字符串,是构成代码的最小单位 如 |
| 生成CSSOM树 | 其实转换 CSS 到 CSSOM 树的过程和上一小节的过程是极其类似 |
| 生成Render Tree | 将以上两者组合为渲染树 但在这一过程中 不是简单的将两者合并就行了 Render Tree只包括需要显示的节点和这些节点的样式信息如果某个节点是 display: none 那么就不会在渲染树中显示。 生成Render Tree以后,就会进行布局(回流——然后调用GPU绘制,合成图层,显示在屏幕上 进行一些底层的硬件操作 |
为什么DOM性能差
因为 DOM是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。
当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
什么情况会阻塞渲染
| 渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。 如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。 |
| 当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。 也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS文件,这也是都建议将 script 标签放在 body 标签底部的原因。 也可以通过给script标签加上async和defer属性 (
) |
回流和重绘
- 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
- 回流是布局或者几何属性需要改变就称为回流。
- 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流
- 当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
- 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
- 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行 requestAnimationFrame回调
- 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback回调
| 使用 transform 替代 top |
| 使用 visibility 替换display: none 因为前者只会引起重绘,后者会引发回流(改变了布局) |
| 不要把节点的属性值放在一个循环里当成循环里的变量 for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流
console.log(document.querySelector('.test').style.offsetTop)
} |
| 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局 |
| 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame |
| CSS 选择符从右往左匹配查找,避免节点层级过多 |
| 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层 |
XSS
跨站脚本攻击
说白了,XSS就是攻击者想尽一切办法将可以执行的代码注入到网页中。
可以使用两种预防方法:
| 转义字符 | 于用户的输入应该是永远不信任的。 最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义 |
| CSP | CSP 本质上就是建立白名单 开发者明确告诉浏览器哪些外部资源可以加载和执行。 我们只需要配置规则,如何拦截是由浏览器自己实现的。 我们可以通过这种方式来尽量减少 XSS 攻击。 可以通过两种方式来开启CSP:
|
CSRF
跨站请求伪造
原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。
如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。
预防方法
预防方法
|
| 设置SameSite: 可以对 Cookie 设置 SameSite 属性。 该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击, 但是该属性目前并不是所有浏览器都兼容。 |
| 验证 Referer: 对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。 |
| Token: 服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效 |
点击劫持
点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击
预防方法:
预防方法:
| X-FRAME-OPTIONS | X-FRAME-OPTIONS 是一个 HTTP 响应头 用来防御iframe 嵌套的点击劫持攻击 该响应头有三个值可选:
|
| JS防御 | 对于某些远古浏览器来说,只有通过 JS 的方式来防御点击劫持了。 <head>
<style id="click-jack">
html {
display: none !important;
}
</style>
</head>
<body>
<script>
if (self == top) {
var style = document.getElementById('click-jack')
document.body.removeChild(style)
} else {
top.location = self.location
}
</script>
</body> |
性能优化
性能优化的内容比较碎片 但总体思路是从两个方面下手:让加载更快 和 让渲染更快
| 加载更快:
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
单例模式
说白了,单例模式就是要求一个类有且只有一个实例
实现方法:
class Singleton {
constructor(name) {
this.name = name;
this.instance = null;
}
// 构造一个广为人知的接口,供用户对该类进行实例化
static getInstance(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
} 函数柯里化
柯里化说白了就是高阶函数的一个特殊用法而已
科学定义:柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
说白了:就是用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数
它的应用场景是参数复用
柯里化的实现:
- 判断当前函数传入的参数是否大于或等于fn需要参数的数量,如果是,直接执行fn
- 如果传入参数数量不够,返回一个闭包,暂存传入的参数,并重新返回currying函数
//柯里化
function currying (fn,...args){
if(args.length >= fn.length){
return fn(...args);
}else{
return (...args2) => currying(fn ,...args,...args2)
}
} 手动实现JSONP
一共分为四步:
- 1.将传入的data数据转化为url字符串形式
- 2.处理url中的回调函数
- 3.创建一个script标签并插入到页面中
- 4.挂载回调函数
(function (window,document) {
"use strict";
var jsonp = function (url,data,callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof('?') == -1? '?': '&';
for(var key in data){
dataString += key + '=' + data[key] + '&';
};
// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
dataString += 'callback=' + cbFuncName;
// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement('script');
scriptEle.src = url + dataString;
// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data);
// 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document) 
京公网安备 11010502036488号