多次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 决定,所以结果永远是 window
this的优先级
要注意:对于 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)