javascript运行原理
- Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
-
- 如果函数没有被调用,那么是不会被转换成AST的;
- Parse的V8官方文档:v8.dev/blog/scanne…
- Ignition是一个解释器,会将AST转换成ByteCode(字节码)
-
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- Ignition的V8官方文档:v8.dev/blog/igniti…
- TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
-
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档:v8.dev/blog/turbof…
上面是JavaScript代码的执行过程,事实上V8的内存回收也是其强大的另外一个原因,这里暂时先不展开讨论:
- Orinoco模块,负责垃圾回收,将程序中不需要的内存回收;
- Orinoco的V8官方文档:v8.dev/blog/trash-…
解释型语言和编译型语言
编译型语言是代码在运行前编译器将人类可以理解的语言(编程语言)转换成机器可以理解的语言。
解释型语言也是人类可以理解的语言(编程语言),也需要转换成机器可以理解的语言才能执行,但是是在运行时转换的。所以执行前需要环境中安装了解释器;但是编译型语言编写的应用在编译后能直接运行。
跨域
疑问: session和cookie的关系?并且后端通过ctx.session.名获取到的session是前端通过localStorage设置的还是后端通过this.ctx.session.名设置的?
- session是保存在服务端的。就是后端设置session,前端请求时,将
withCredentials
设置为true,就会发送cookie到服务端。 - 服务端执行session机制时候会生成session的id值,这个id值会发送给客户端,客户端每次请求都会把这个id值放到http请求的头部发送给服务端,而这个id值在客户端会保存下来,保存的容器就是cookie,因此当我们完全禁掉浏览器的cookie的时候,服务端的session也会不能正常使用。
- Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
- Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
cors设置
在后端设置一些特定的响应头。
//设置哪个源可以访问
ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
//允许携带cookie
ctx.set("Access-Control-Allow-Credentials", true);
//允许那些方法访问我
ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
//允许携带那个头部访问我
ctx.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, cc");
关于 cors 的 cookie 问题
想要传递 cookie
需要满足 3 个条件
1.web 请求设置withCredentials
这里默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials
来进行传递 cookie
.
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
复制代码
2.Access-Control-Allow-Credentials
为 true
3.Access-Control-Allow-Origin
为非 *
这里请求的方式,在 chrome
中是能看到返回值的,但是只要不满足以上其一,浏览器会报错,获取不到返回值。
正向代理
代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。
就是在各个框架的json文件中,设置一个proxy选项。不同框架设置不同,所以用到时,自己搜索即可。 xxx proxy
JSONP
JSONP
主要就是利用了 script
标签没有跨域限制的这个特性来完成的。只支持get方法。
原理:JSONP 的理念就是,与服务端约定好一个回调函数名,服务端接收到请求后,将返回一段 Javascript,在这段 Javascript 代码中调用了约定好的回调函数,并且将数据作为参数进行传递。当网页接收到这段 Javascript 代码后,就会执行这个回调函数,这时数据已经成功传输到客户端了。
前端代码
前端定义一个回调函数,然后通过请求路径将该回调当成参数传递。
// 1. 创建回调函数callback
function Callback(res) {
alert(JSON.stringify(res, null , 2));
}
document.getElementById('btn-4').addEventListener('click', function() {
// 2. 动态创建script标签,并设置src属性,注意参数cb=Callback
var script = document.createElement('script');
script.src = 'http://127.0.0.1:3000/api/jsonp?cb=Callback';
document.getElementsByTagName('head')[0].appendChild(script);
});
后端代码
通过前端传过来的回调函数,我们将返回该函数的调用。
router.get('/api/jsonp', (req, res, next) => {
var str = JSON.stringify(data);
// 3. 创建script脚本内容,用`callback`函数包裹住数据
// 形式:callback(data)
var script = `${req.query.cb}(${str})`;//这里就是callback(data),当前端请求接口时,就会回调该函数
res.send(script);
});
// 4. 前端收到响应数据会自动执行该脚本
WebSocket
这种方式本质没有使用了 HTTP 的响应头, 因此也没有跨域的限制,没有什么过多的解释直接上代码吧。
前端代码
<script>
let socket = new WebSocket("ws://localhost:80");
socket.onopen = function() { socket.send("返回的内容"); };
socket.onmessage = function(e) { console.log(e.data); };
</script>
nginx反向代理
以后再学,设计nginx知识。
鉴权
cookie
服务端通过set-cookie设置cookie的信息。
Session
根据以上流程可知,session通过cookie来传递sessionId,达到用户鉴权的目的。除此之外,sessionId也可以不通过cookie传递,比如通过response返回客户端,再当作请求的参数传递给服务器去验证。
session-cookie
当需要登录后才可以操作其他的。需要后端设置session, 前端请求时,将服务器返回的session id保存在cookie中。
token
Token认证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个
Token
,再把这个Token
发送给客户端 - 客户端收到
Token
以后可以把它存储起来,比如放在Cookie
里或者Local Storage
里 - 客户端每次向服务端请求资源的时候需要带着服务端签发的
Token
- 服务端收到请求,然后去验证客户端请求里面带着的
Token
(request头部添加Authorization),如果验证成功,就向客户端返回请求的数据 ,如果不成功返回401错误码,鉴权失败。
//前端
axios.interceptors.request.use(config => {
config.headers.Authorization = window.sessionStorage.getItem("token")
return config
})
jwt(基于token)
基于 token
的解决方案有许多,常用的是JWT
,JWT
的原理是,服务器认证以后,生成一个 JSON
对象,这个 JSON
对象肯定不能裸传给用户,那谁都可以篡改这个对象发送请求。因此这个 JSON
对象会被服务器端签名加密后返回给用户,返回的内容就是一张令牌,以后用户每次访问服务器端就带着这张令牌。
jwt的组成:Header(头部)、Payload(负载)、Signature(签名)。
- Header部分是一个JSON对象,描述JWT的元数据。一般描述信息为该Token的加密算法以及Token的类型。{"alg": "HS256","typ": "JWT"}的意思就是,该token使用HS256加密,token类型是JWT。这个部分基本相当于明文,它将这个JSON对象做了一个Base64转码,变成一个字符串。Base64编码解码是有算法的,解码过程是可逆的。头部信息默认携带着两个字段。
- Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。有7个官方字段,还可以在这个部分定义私有字段。一般存放用户名、用户身份以及一些JWT的描述字段。它也只是做了一个Base64编码,因此肯定不能在其中存放秘密信息,比如说登录密码之类的。
- Signature是对前面两个部分的签名,防止数据篡改,如果前面两段信息被人修改了发送给服务器端,此时服务器端是可利用签名来验证信息的正确性的。签名需要密钥,密钥是服务器端保存的,用户不知道。算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
//前端代码
//axios的请求拦截器,在每个request请求头上加JWT认证信息
axios.interceptors.request.use(
config => {
const token = window.localStorage.getItem("token");
if (token) {
// 判断是否存在token,如果存在的话,则每个http header都加上token
// Bearer是JWT的认证头部信息
config.headers.common["Authorization"] = "Bearer " + token;
}
return config;
},
err => {
return Promise.reject(err);
}
);
//登录方法:在将后端返回的JWT存入localStorage
async login() {
const res = await axios.post("/login-token", {
username: this.username,
password: this.password
});
localStorage.setItem("token", res.data.token);
},
//登出方法:删除JWT
async logout() {
localStorage.removeItem("token");
},
async getUser() {
await axios.get("/getUser-token");
}
//后端代码
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
//用来签名的密钥
const secret = "it's a secret";
router.post("/login-token", async ctx => {
const { body } = ctx.request;
//登录逻辑,略,即查找数据库,若该用户和密码合法,即将其信息生成一个JWT令牌传给用户
const userinfo = body.username;
ctx.body = {
message: "登录成功",
user: userinfo,
// 生成 token 返回给客户端
token: jwt.sign(
{
data: userinfo,
// 设置 token 过期时间,一小时后,秒为单位
exp: Math.floor(Date.now() / 1000) + 60 * 60
},
secret
)
};
});
//jwtAuth这个中间件会拿着密钥解析JWT是否合法。
//并且把JWT中的payload的信息解析后放到state中,ctx.state用于中间件的传值。
router.get(
"/getUser-token",
jwtAuth({
secret
}),
async ctx => {
// 验证通过,state.user
console.log(ctx.state.user);
ctx.body = {
message: "获取数据成功",
userinfo: ctx.state.user.data
};
}
)
//这种密码学的方式使得token不需要存储,只要服务端能拿着密钥解析出用户信息,就说明该用户是合法的。
//若要更进一步的权限验证,需要判断解析出的用户身份是管理员还是普通用户。
疑问:token后端生成后,放在哪里,是通过数据传递给前端,还是通过本地存储技术将其存储到本地,然后前端访问时,取出然后再通过authorization请求头传递给后端?
答: 是通过数据传递给前端,前端保存在本地,在以后需要权限才可以访问的时候,携带即可。
预编译*
-
找函数里面的变量声明和形参,此时赋值为undefined
-
形参和实参相统一,就是给形参赋值
-
找函数声明,如果有与函数同名的变量和函数,函数将覆盖变量
-
然后再按照上下顺序执行代码,遇到相同的变量名和函数名,就相互覆盖
-
然后再找赋值语句对相应变量赋值
总之,变量的声明提升早与函数的声明提升
var bar = []; // 定义一个数组
for(var i = 0;i < 10;i++){
bar[i] = function(){ // 每个数组元素定义为一个函数
console.log(i) // 函数体
}
}
bar[1](); // 10
bar[2](); // 10,都是输出10,深入理解需要掌握“预编译”和“作用域”的知识,
// 思考方向 => 函数执行前,存在函数预编译AO(Activation Object)对象
如何验证let,const
声明的变量也存在变量提升
let,const
创建的过程被提升、初始化没有提升
其实他们声明的变量也是存在声明提升的,只是存在暂时性死区,不能在声明之前访问而已。如下面的例子,如果他不存在声明提升,那么这不会报错,会输出zh
let name = 'zh'
{
console.log(name) // Uncaught ReferenceError: name is not defined
let name = 'hy'
}
负载均衡
当用户访问网站的时候,先访问一个中间服务器,再让这个中间服务器在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入选择的服务器。这样保证服务器集群中的每个服务器压力趋于平衡。
执行上下文
JavaScript 中有三种执行上下文类型。
- 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 - 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
- Eval 函数执行上下文 — 执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval
。
执行栈
就是当程序开始运行的时候,js引擎会创建一个栈结构,并且创建一个全局的执行上下文将其压入栈中,遇到函数调用时,会创建一个函数执行上下文,将其压入栈中,以此类推。函数执行完毕,将该函数的执行上下文从栈中弹出,将执行权交给栈顶元素。直到该程序执行完毕,将全局执行上下文从栈中弹出。
调用栈是一种数据结构。如果我们运行到一个函数,它就会将其放置到栈顶。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。
作用域
作用域链是依靠执行上下文连接的,环境栈中的变量对象,从上到下就组成一条作用域链。他的用途就是保证对执行环境有权访问的所有变量和函数的有序访问。
词法作用域是指内部函数在定义的时候就决定了其外部作用域。
(function autorun(){
let x = 1;
//这个函数中访问变量的时候,作用域是定义的时候决定的,而不是执行的时候。
function log(){
console.log(x);
};
function run(fn){
let x = 100;
fn();
}
run(log);//1
})();
词法环境
包含环境记录器和外部环境的引用
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域)。
js的事件循环
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
MacroTask(宏任务)
script
全部代码、setTimeout
、setInterval
、setImmediate
(浏览器暂时不支持,只有IE10支持,具体可见MDN
)、I/O
、UI Rendering
。
MicroTask(微任务)
Process.nextTick(Node独有)
、Promise
的then回调、Object.observe(废弃)
、MutationObserver
(具体使用方式查看这里)
setTimeout
这个函数,是经过指定时间后,把要执行的任务加入到Event Queue中。并不是到了指定的时间就执行,只有当主线程空闲出来后,才回去执行event queue中的等待事件。
setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
宏任务和微任务的执行顺序:
执行项目逻辑 ----> 将setTimeout,setInterval等定义的回调函数加入到宏任务队列,将promiese执行的回调加入到微任务队列 -------> 主逻辑执行完毕 -------> 执行微任务队列中的回调 --------> 执行宏任务队列中的回调
整段script代码是一个宏任务,在执行宏任务的过程中会不断的有微任务加入到微任务队列中,当执行完一个宏任务后先看微任务队列里有没有微任务,如果有先把整队的微任务执行完,然后在执行下一个宏任务,如此以往形成event loop。
process.nextTick()
虽然它是异步API的一部分。process.nextTick()
从技术上讲,它不是事件循环的一部分。
process.nextTick()
方法将callback
添加到next tick
队列。 一旦当前事件轮询队列的任务全部完成,在next tick
队列中的所有callbacks
会被依次调用。- process.nextTick应该是独立于Event Loop 之外的,它是微任务,但是它本身应该有一个自己的队列,这个队列中的回调函数会优先于微任务队列中的函数执行。比如,你把process.nextTick放在Promise.then的下方,他还是会优先执行。
//注意promise改变状态之前,他是在主线程执行的,在then,和await中是在微任务中执行的。
console.log('1');
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
// process.nextTick(function () {
// console.log('3');
// })
})
process.nextTick(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
setTimeout(async function () {
console.log('9');
// process.nextTick(function () {
// console.log('10');
// })
// new Promise(function (resolve) {
// console.log('11');
// resolve();
// }).then(function () {
// console.log('12')
// })
let result = await Promise.resolve("11")
console.log(result)
console.log("12")
})
; (async () => {
console.log('13');
let result = await Promise.resolve("14")
console.log(result)
console.log("15")
})()
// 1 7 13 6 8 14 15 2 4 3 5 9 11 12。
mutationobserver
Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。
概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
Mutation Observer 有以下特点。
- 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
- 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
- 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。
MutationObserver构造函数
通过new创建一个观察者对象,改构造函数需要传递一个回调函数。
var observer = new MutationObserver(callback);
该回调函数接收两个参数,参一:变动dom的数组,参二:观察者实例。
let callback = function (mutations, self) {
}
实例方法:
1.observe(变动元素,观察选项)
let odiv = document.getElementById('#div');
let options = {
'childList': true,
'attributes':true
} ;
observer.observe(odiv, options);
以下是 options对象的各属性及其描述:
属性 | 类型 | 描述 | childList | Boolean | 是否观察子节点的变动 | attributes | Boolean | 是否观察属性的变动 | characterData | Boolean | 是否节点内容或节点文本的变动 | subtree | Boolean | 是否观察所有后代节点的变动 | attributeOldValue | Boolean | 观察 attributes 变动时,是否记录变动前的属性值 | characterDataOldValue | Boolean | 观察 characterData 变动时,是否记录变动前的属性值 | attributeFilter | Array | 表示需要观察的特定属性(比如['class','src']),不在此数组中的属性变化时将被忽略 |
---|
注意:
-
对一个节点添加观察器,就像使用
addEventListener
方法一样,多次添加同一个观察器是无效的,回调函数依然只会触发一次。但是,如果指定不同的options
对象,就会被当作两个不同的观察器。 -
监听选项中必须指定 childList、attributes 和 characterData 中的一种或多种。
Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true.
2.disconnect()
该方法是停止监听dom变化的。
observer.disconnect();
3.takeRecords()
用来清除变动记录,即不再处理未处理的变动。该方法返回变动记录的数组。
// 保存所有没有被观察器处理的变动
observer.takeRecords();
MutationRecord 对象
即每次dom发生变化所产生的状态值。
属性 | 类型 | 描述 | type | String | 根据变动类型,值为 attributes, characterData 或 childList | target | Node | 发生变动的DOM节点 | addedNodes | NodeList | 被添加的节点,或者为 null | removedNodes | NodeList | 被删除的节点,或者为 null | previousSibling | Node | 被添加或被删除的节点的前一个兄弟节点,或者为 null | nextSibling | Node | 被添加或被删除的节点的后一个兄弟节点,或者为 null | attributeName | String | 发生变更的属性的本地名称,或者为 null | attributeNamespace | String | 发生变更的属性的命名空间,或者为 null | oldValue | String | 如果 type 为 attributes,则返回该属性变化之前的属性值;如果 type 为 characterData,则返回该节点变化之前的文本数据;如果 type为 childList,则返回 null |
---|
优化js中的逻辑判断
普通的写法:if-else if-else, switch-case
一元判断:Map, 对象
Map: [[条件, 逻辑参数],[条件, 逻辑参数],...]
对象: {条件:逻辑参数}
/**
* 按钮点击事件
* @param {number} status 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消
*/
const onButtonClick = (status)=>{
if(status == 1){
sendLog('processing')
jumpTo('IndexPage')
}else if(status == 2){
sendLog('fail')
jumpTo('FailPage')
}else if(status == 3){
sendLog('fail')
jumpTo('FailPage')
}else if(status == 4){
sendLog('success')
jumpTo('SuccessPage')
}else if(status == 5){
sendLog('cancel')
jumpTo('CancelPage')
}else {
sendLog('other')
jumpTo('Index')
}
}
const actions = {
'1': ['processing','IndexPage'],
'2': ['fail','FailPage'],
'3': ['fail','FailPage'],
'4': ['success','SuccessPage'],
'5': ['cancel','CancelPage'],
'default': ['other','Index'],
}
/**
* 按钮点击事件
* @param {number} status 活动状态:1开团进行中 2开团失败 3 商品售罄 4 开团成功 5 系统取消
*/
const onButtonClick = (status)=>{
let action = actions[status] || actions['default'],
logName = action[0],
pageName = action[1]
sendLog(logName)
jumpTo(pageName)
}
const actions = new Map([
[1, ['processing','IndexPage']],
[2, ['fail','FailPage']],
[3, ['fail','FailPage']],
[4, ['success','SuccessPage']],
[5, ['cancel','CancelPage']],
['default', ['other','Index']]
])
/**
* 按钮点击事件
* @param {number} status 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消
*/
const onButtonClick = (status)=>{
let action = actions.get(status) || actions.get('default')
sendLog(action[0])
jumpTo(action[1])
}
多层判断:Map
Map: [[条件,() => {}], [条件,() => {}], ....]
- 条件可以是多个条件合并的字符串
- 条件可以是一个对象
- 条件可以是正则,为了匹配更多相同逻辑不同条件的处理
- .....
对象:{条件:() => {}, 条件:() => {}, ...}
- 条件可以是多个条件合并的字符串
/**
* 按钮点击事件
* @param {number} status 活动状态:1开团进行中 2开团失败 3 开团成功 4 商品售罄 5 有库存未开团
* @param {string} identity 身份标识:guest客态 master主态
*/
const onButtonClick = (status,identity)=>{
if(identity == 'guest'){
if(status == 1){
//do sth
}else if(status == 2){
//do sth
}else if(status == 3){
//do sth
}else if(status == 4){
//do sth
}else if(status == 5){
//do sth
}else {
//do sth
}
}else if(identity == 'master') {
if(status == 1){
//do sth
}else if(status == 2){
//do sth
}else if(status == 3){
//do sth
}else if(status == 4){
//do sth
}else if(status == 5){
//do sth
}else {
//do sth
}
}
}
const actions = new Map([
['guest_1', ()=>{/*do sth*/}],
['guest_2', ()=>{/*do sth*/}],
['guest_3', ()=>{/*do sth*/}],
['guest_4', ()=>{/*do sth*/}],
['guest_5', ()=>{/*do sth*/}],
['master_1', ()=>{/*do sth*/}],
['master_2', ()=>{/*do sth*/}],
['master_3', ()=>{/*do sth*/}],
['master_4', ()=>{/*do sth*/}],
['master_5', ()=>{/*do sth*/}],
['default', ()=>{/*do sth*/}],
])
/**
* 按钮点击事件
* @param {string} identity 身份标识:guest客态 master主态
* @param {number} status 活动状态:1 开团进行中 2 开团失败 3 开团成功 4 商品售罄 5 有库存未开团
*/
const onButtonClick = (identity,status)=>{
let action = actions.get(`${identity}_${status}`) || actions.get('default')
action.call(this)
}
const actions = {
'guest_1':()=>{/*do sth*/},
'guest_2':()=>{/*do sth*/},
//....
}
const onButtonClick = (identity,status)=>{
let action = actions[`${identity}_${status}`] || actions['default']
action.call(this)
}
const actions = new Map([
[{identity:'guest',status:1},()=>{/*do sth*/}],
[{identity:'guest',status:2},()=>{/*do sth*/}],
//...
])
const onButtonClick = (identity,status)=>{
// ...actions表示将[[],[],[]]内部的数组取出[],[],[]
let action = [...actions].filter(([key,value])=>(key.identity == identity && key.status == status))
action.forEach(([key,value])=>value.call(this))
}
const actions = ()=>{
const functionA = ()=>{/*do sth*/}
const functionB = ()=>{/*do sth*/}
return new Map([
[{identity:'guest',status:1},functionA],
[{identity:'guest',status:2},functionA],
[{identity:'guest',status:3},functionA],
[{identity:'guest',status:4},functionA],
[{identity:'guest',status:5},functionB],
//...
])
}
const onButtonClick = (identity,status)=>{
let action = [...actions()].filter(([key,value])=>(key.identity == identity && key.status == status))
action.forEach(([key,value])=>value.call(this))
}
const actions = ()=>{
const functionA = ()=>{/*do sth*/}
const functionB = ()=>{/*do sth*/}
const functionC = ()=>{/*send log*/}
return new Map([
[/^guest_[1-4]$/,functionA],
[/^guest_5$/,functionB],
[/^guest_.*$/,functionC],
//...
])
}
const onButtonClick = (identity,status)=>{
let action = [...actions()].filter(([key,value])=>(key.test(`${identity}_${status}`)))
action.forEach(([key,value])=>value.call(this))
}
promise
常用的静态方法:all,race,resolve, reject。
常用的实例方法:then, catch。
对于all方法来说,参数是一个若干个promise组成的数组,如果这些promise定义了自己的then,catch方法,并且有返回值,不管是成功还是失败,那么执行all方法的then就会执行,catch就不会执行。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(res => res)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch();//这里没有返回值,才会执行all的catch方法。
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
//执行结果
Error: 报错了
axios对其还做了一步简化。将接收到的请求参数封装成一个函数接收spread()
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));
promise只执行一次,但是同一个promise的then,catch可以被执行多次。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once')
resolve('success')
}, 1000)
})
const start = Date.now()
promise.then((res) => {
console.log(res, Date.now() - start)
})
promise.then((res) => {
console.log(res, Date.now() - start)
})
//只会打印出一次once
.then
或者 .catch
的参数期望是函数,传入非函数则会发生值穿透。
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
.then
抛出的错误不会被第二个参数函数捕获,只能被后面的catch捕获。
Promise.resolve()
.then(function success (res) {
//注意这里是抛出错误,而非返回错误。
throw new Error('error')
}, function fail1 (e) {
console.error('fail1: ', e)
})
.catch(function fail2 (e) {
console.error('fail2: ', e)
})
手写promise
class Promise {
constructor(executor) {//执行器函数
let _this = this
_this.status = 'pending' //保存当前状态
_this.value = undefined //执行成功时,传递的值
_this.error = undefined//执行失败时,传递的值
function resolve(value) {
if (_this.status === 'pending') {
_this.status = 'resolved'
_this.value = value
}
}
function reject(error) {
if (_this.status === 'pending') {
_this.status = 'rejected'
_this.error = error
}
}
executor(resolve, reject)//调用执行器函数
}
then(onfulfilled, onRejected) {
let _this = this
if (_this.status === 'resolved') {
onfulfilled(_this.value)
} else if (_this.status === 'rejected') {
onRejected(_this.error)
}
}
}
//测试,此代码的问题是不能实现异步方法
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('我是张昊')
reject('我是李龙淼')
}, 1000)
}).then(res => {
console.log(res)
}, res => {
console.log(res)
})
//这里什么也不输出
原因是我们在then函数中只对成功态和失败态进行了判断,而实例被new时,执行器中的代码会立即执行,但setTimeout中的代码将稍后执行,也就是说,then方法执行时,Promise的状态没有被改变依然是pending态,所以我们要对pending态也做判断,而由于代码可能是异步的,那么我们就要想办法把回调函数进行缓存,并且,then方法是可以多次使用的,所以要能存多个回调,那么这里我们用一个数组。
创建两个数组,为了存储成功的回调(resolve)和失败的回调(reject)
_this.onResolvedCallbacks = []; // 存放then成功的回调,这里存的都是resolve()的函数
_this.onRejectedCallbacks = []; // 存放then失败的回调,这里存的都是reject()的函数
在then中判断pending的判断,将then中的回调分别存入onResolvedCallbacks
和onRejectedCallbacks
数组中
if (_this.status === 'pending') {
// 每一次then时,如果是等待态,就把回调函数push进数组中,什么时候改变状态什么时候再执行
//缓存成功时的回调函数,等到状态变为resolved的时候,在执行
_this.onResolvedCallbacks.push(function () {
onfulfilled(_this.value)
})
//缓存失败时的回调函数,等到状态变为rejected的时候,在执行
_this.onRejectedCallbacks.push(function () {
onRejectedCallbacks(_this.error)
})
}
在resolve方法中缓存异步执行的成功回调,在reject方法中缓存异步执行的失败的回调
function resolve(value) {
if (_this.status === 'pending') {
_this.status = 'resolved'
_this.value = value
// 当成功的函数 (resolve()) 被调用时,之前缓存的回调函数会被一一调用
_this.onResolvedCallbacks.map(callback => callback())
}
}
function reject(error) {
if (_this.status === 'pending') {
_this.status = 'rejected'
_this.error = error
// 当失败的函数 (reject()) 被调用时,之前缓存的回调函数会被一一调用
_this.onRejectedCallbacks.map(callback => callback())
}
}
为了防止promise在执行出现错误,我们需要做错误处理
try {
executor(resolve, reject)
} catch (e) {
console.log(e)
}
promise的链式调用。由于promise的状态会发生改变,所以then返回的promise是一个全新的。
有点复杂!!! promise的其他原型方法
// 捕获错误的方法,在原型上有catch方法,返回一个没有resolve的then结果即可
Promise.prototype.catch = function (callback) {
return this.then(null, callback)
}
// 解析全部方法,接收一个Promise数组promises,返回新的Promise,遍历数组,都完成再resolve
Promise.all = function (promises) {
//promises是一个promise的数组
return new Promise(function (resolve, reject) {
let arr = []; //arr是最终返回值的结果
let i = 0; // 表示成功了多少次
function processData(index, y) {
arr[index] = y;
if (++i === promises.length) {
resolve(arr);
}
}
for (let i = 0; i < promises.length; i++) {
promises[i].then(function (y) {
processData(i, y)
}, reject)
}
})
}
// 只要有一个promise成功了 就算成功。如果第一个失败了就失败了
Promise.race = function (promises) {
return new Promise(function (resolve, reject) {
for (var i = 0; i < promises.length; i++) {
promises[i].then(resolve,reject)
}
})
}
// 生成一个成功的promise
Promise.resolve = function(value){
return new Promise(function(resolve,reject){
resolve(value);
})
}
// 生成一个失败的promise
Promise.reject = function(error){
return new Promise(function(resolve,reject){
reject(error);
})
}
前端的路由跳转
Hash 方法是在路由中带有一个 #
,主要原理是通过监听 #
后的 URL 路径标识符的更改而触发的浏览器 hashchange
事件,然后通过获取 location.hash
得到当前的路径标识符,再进行一些路由跳转的操作。
class RouterClass {
constructor() {
this.isBack = false
this.routes = {} // 记录路径标识符对应的cb
this.currentUrl = '' // 记录hash只为方便执行cb
this.historyStack = [] // hash栈
window.addEventListener('load', () => this.render())
window.addEventListener('hashchange', () => this.render())
}
/* 初始化 */
static init() {
window.Router = new RouterClass()
}
/* 记录path对应cb */
route(path, cb) {
this.routes[path] = cb || function() {}
}
/* 入栈当前hash,执行cb */
render() {
if (this.isBack) { // 如果是由backoff进入,则置false之后return
this.isBack = false // 其他操作在backoff方法中已经做了
return
}
this.currentUrl = location.hash.slice(1) || '/'
//将每一个路径都加入到栈中,为了back的判断
this.historyStack.push(this.currentUrl)
this.routes[this.currentUrl]()
}
/* 路由后退 */
back() {
this.isBack = true
this.historyStack.pop() // 移除当前hash,回退到上一个
const { length } = this.historyStack
if (!length) return //如果栈中没有路径了,将直接结束
let prev = this.historyStack[length - 1] // 拿到要回退到的目标hash
location.hash = `#${ prev }` //为了使手动跳转正常进行,需要将当前路径加上一个#,来满足slice的分割
this.currentUrl = prev
this.routes[prev]() // 执行对应cb
}
}
history
history.go(n)
:路由跳转,比如n为2
是往前移动2个页面,n为-2
是向后移动2个页面,n为0是刷新页面history.back()
:路由后退,相当于history.go(-1)
history.forward()
:路由前进,相当于history.go(1)
history.pushState()
:添加一条路由历史记录,如果设置跨域网址则报错,浏览器有记录。history.replaceState()
:替换当前页在路由历史记录的信息,浏览器无记录。popstate
事件:当活动的历史记录发生变化,就会触发popstate
事件,在点击浏览器的前进后退按钮或者调用上面前三个方法的时候也会触发,参见 MDN
何为闭包?
闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用。
而闭包则意味着同时包括函数指针和环境两个关键元素。在编译优化当中,没有捕捉自由变量的闭包可以被优化成普通函数。
闭包中引入的变量何时被销毁?
闭包中访问的外部变量是存放在堆内存中的。
变量的生命周期取决于闭包的生命周期。被闭包引用的外部作用域中的变量将一直存活直到闭包函数被销毁。如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁。
数组方法(存在高阶函数)的返回值
map:该方法的返回值就是回调函数的返回值组成的数组。
filter: 该方法的返回值就是回调函数符合条件的返回值组成的数组。
let arr = [1, 2, 3, 4]
let resArr = arr.filter(item => {
if (item > 2) {
return item
}
})
//等价于
arr.filter(item => item > 2)
console.log(resArr)
forEach:该方法没有返回值。
reduce:该方法返回的值是累计器(回调函数的第一个参数)累计后的值。
let arr = [
{ id: 1, name: 'zh', age: 20 },
{ id: 2, name: 'hy', age: 19 },
{ id: 3, name: 'llm', age: 19 }
]
let reArr = arr.reduce((pre, next) => {
if (next.age === 19) {
return pre.concat(Object.assign({}, next, { sex: 'female' }))
}
return []//这里必须有返回值,不然会报错,因为每遍历一项都需要有返回值。如果上面用到了pre,下面没有返回就会报错,pre的值是每次回调函数返回的值。
}, [])
console.log(reArr)
// {id: 2, name: "hy", age: 19, sex: "female"}
// {id: 3, name: "llm", age: 19, sex: "female"}
every: 该方法的返回值是布尔值,只有数组中的元素都满足条件,才回返回true,否则返回false。
let arr = [1, 2, 3, 4]
let resArr = arr.every(item => item >= 1)
console.log(resArr)
some:该方法返回的是布尔值,表示只要数组中有一个元素满足条件就返回true,否则返回false。
find:该方法返回的是第一个满足条件的值。
findIndex:该方法返回的是第一个满足条件的值的下标。
URLsearchParams
URLsearchParams(url)用来解析url参数的。
//URLSearchParams用来解析参数的
let url = '?name=zh&age=20';
let searchParams = new URLSearchParams(url)
// console.log(searchParams)//map对象
let arr = [...searchParams]
console.log(arr) //[ [ 'name', 'zh' ], [ 'age', '20' ] ]
获取单个参数。
searchParams.get('name')
校验参数是否存在。
searchParams.has('sex') // false
searchParams.has('age') // true
添加参数。
searchParams.append('sex', 'male')
console.log(searchParams)//URLSearchParams { 'name' => 'zh', 'age' => '20', 'sex' => 'male' }
console.log(url)//?name=zh&age=20,注意url并不会添加上该参数,但是解析后是有该参数的
删除参数。
searchParams.delete('sex');
searchParams.has('sex'); // false
修改参数。
searchParams.set('age', 22)
console.log(searchParams)//URLSearchParams { 'name' => 'zh', 'age' => '22', 'sex' => 'male' }
将解析后的参数,再转为查询字符串。
searchParams.toString() //name=zh&age=22&sex=male
重写数组方法
重写map方法,利用for循环
const selfMap = function (fn, context = this) {
//这里的context会被当做fn函数中的this,如果不传入context,那么this就是调用的数组。
let arr = Array.prototype.slice.call(context)
let mappedArr = Array()
for (let i = 0; i < arr.length; i++) {
//判断稀疏数组的情况
if (!arr.hasOwnProperty(i)) continue;
mappedArr[i] = fn.call(context, arr[i], i, this)
// console.log(context)
}
return mappedArr
}
Array.prototype.selfMap = selfMap
let resArr = [0, 0, 0, 1].selfMap(number => number * 2, [2, 3, 4])
console.log(resArr)//[4,6,8]
我对map方法的错误理解,context只是改变了this指向,然而并不是改变函数执行的数组。
作者大大,第二个方法,即map方法,我感觉应该把context初始为this,然后slice方法中传入context,这样传入第二个参数的时候,this的值才回改变。不然没有效果 let resArr = [0, 0, 0, 1].selfMap(number => number * 2, [2, 3, 4]) console.log(resArr)//这里仍然是[0,0,0,2],而不是[4,6,8] //下面这样就是对的 const selfMap = function (fn, context = this) { //这里的context会被当做fn函数中的this let arr = Array.prototype.slice.call(context) let mappedArr = Array() for (let i = 0; i number * 2, [2, 3, 4]) console.log(resArr)//[4,6,8] 如果不对,望告知。
这里是js原生的map测试
let resArr = [4, 5, 6].map(function (item, index, arr1) {
console.log(arr1)//[4,5,6]
console.log('this', this)//[1,2,3]
return item * 2//作用的是[4,5,6]
}, [1, 2, 3])
console.log(resArr)
重写map方法。利用reduce方法。
const selfMap = function (fn, context) {
let arr = Array.prototype.slice.call(this)
return arr.reduce((pre, cur, index) => {
//这个返回值就是pre,然后将完全处理后的元素,都返回给pre,然后展开pre即可。
//也就是这里我有一个疑问,他每次都返回pre,每次都展开pre,每次展开都会重复上一次返回的pre中的元素呀,所以应该会重复很多呀。
return [...pre, fn.call(context, cur, index, this)]
}, [])
}
Array.prototype.selfMap = selfMap
let resArr = [1, 2, 3].selfMap(item => item * 2)
console.log(resArr)
解决疑惑:因为数组的内存地址都是一样的,pre的内存地址一样的,只是展开了最后一次赋值的值。
let a = new Array(10)
let resArr = []
for (let i = 0; i <= a.length; i++) {
let arr = [i]
resArr = [...arr, 2, 3, 4]
}
console.log(resArr)//[ 10, 2, 3, 4 ]
重写filter方法,利用for循环。
const selfFilter = function (fn, context) {
//要处理的数组
let arr = Array.prototype.slice.call(this)
let resArr = Array()
for (let i = 0; i < arr.length; i++) {
//判断返回的条件是否正确,并且加入到数组中。
fn.call(context, arr[i], i, this) && resArr.push(arr[i])
}
return resArr
}
Array.prototype.selfFilter = selfFilter
let arr = [1, 2, 3]
let arr1 = arr.selfFilter(item => item > 2)
console.log(arr1)
重写filter,利用reduce。
const selfFilter = function (fn, context) {
let arr = Array.prototype.slice.call(this)
let resArr = []
return arr.reduce((pre, cur, index) => {
// if (fn.call(context, cur, index, this)) {
// resArr.push(cur)
// return resArr
// }
return fn.call(context, cur, index, this) ? [...pre, cur] : [...pre]
}, [])
}
Array.prototype.selfFilter = selfFilter
let arr = [0, 2, 3]
let arr1 = arr.selfFilter(item => item > 2)
console.log(arr1)
重写some,利用for循环。
const someFilter = function (fn, context) {
let arr = Array.prototype.slice.call(this)
for (let i = 0; i < arr.length; i++) {
if (fn.call(context, arr[i], i, this))
return true
}
return false
}
Array.prototype.someFilter = someFilter
let arr = [1, 2, 3]
let arr1 = arr.someFilter(item => item > 2)
console.log(arr1)
重写flat方法,利用reduce。
const selfFlat = function (depth = 1) {
let arr = Array.prototype.slice.call(this)
// let resArr = []
if (depth === 0) return arr
return reduce((pre, cur) => {
if (Array.isArray(cur)) {
//反正就是递归
return [...pre, ...selfFlat.call(cur, depth - 1)]
} else {
return [...pre, cur]
}
}, [])
}
es6的面向对象class语法
ES6 的 class 内部是基于寄生组合式继承
function inherit(subType, superType) {
subType.prototype = Object.create(superType.prototype, {
constructor: {
value: subType,
enumerable: false,
configurable: true,
writable: true
}
})
// 让类的静态方法也可以被继承。
Object.setPrototypeOf(subType, superType)
}
函数的柯里化
j就是通过以下调用的函数,可以实现分步调用。
他就是一个三阶函数。会后直接调用函数。
柯里化可是将一个多参数的函数转换成多个单参数的函数,但是现在我们不仅可以传入一个参数,还可以一次传入两个参数。
var curry = function (fn) {
//这一步是将传入的参数除了fn以外的参数抽选出来。
var args = [].slice.call(arguments, 1);
return function () {
//将传入的所有的参数连接在一起。
var newArgs = args.concat([].slice.call(arguments));
//其实我们分开调用函数,最后也是直接调用了最后一个函数。
return fn.apply(this, newArgs);
};
};
这里修改了一些bug。既可以一次调用,也可以多次调用。
function curry(fn, args) {
//访问fn函数中参数的个数。
let length = fn.length;
args = args || []
return function () {
let _args = args.slice(0), arg, i;
for (i = 0; i < arguments.length; i++) {
//将第二个函数中的参数加入到第一个参数中去。
arg = arguments[i];
_args.push(arg)
}
//比较总的参数和fn参数。直到函数参数满足被柯里化函数参数相同时调用。
if (_args.length < length) {
return curry.call(this, fn, _args);
} else {
return fn.apply(this, _args)
}
}
}
let fn = curry(function (a, b, c) {
console.log([a, b, c])
})
fn("a", "b", "c")
fn("a")("c", "b", "d")
函数的length和arguments
注意:length表示的是形参的个数,而arguments表示的是实参的个数。
Virtual Dom 的优势在哪里?
「Virtual Dom 的优势」其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。
首先我们需要知道:
DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程) JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化, 引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。
其次是 VDOM 和真实 DOM 的区别和优化:
- 虚拟 DOM 不会立马进行排版与重绘操作
- 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
- 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部
如何选择图片
名称 | 介绍 | 不适合 | 适合 | JPEG | 在互联网上常被应用于存储和传输照片 | 线条图形,文字,图标图形,它不支持透明度 | 颜色丰富的照片,彩色图大焦点图,通栏bannr图,结构不规则的图形。 | PNG | 栅格图形,最初是为了代替gif设计的,文件比较大,支持半透明和透明的特性。 | 由于无损存储,彩色图像体积太大,所以不太适合 | 纯色,透明,线条绘图,图标。边缘清晰,有大块相同颜色区域。颜色数较少但是需要半透明。 | Webp | 可以插入多帧,实现动画效果,可以设置透明度,比gif有更好的动画。 | 最多处理256色,不适合于彩色图片 | 适用于图形和半透明图像 | Gif | 栅格图形,支持256色,仅支持完全透明和完全不透明,如果需要比较通用的动画,gif是唯一的选择。 | 每个像素只有8比特,不适合存储彩色图片。 | 动画和图标 |
---|
JSON.stringify()
以下情况会出现失真。
如果对象含有 toJSON 方法会调用 toJSON。
const arr = [function () { }, undefined, NaN, 2]
arr.toJSON = () => {
return '你失真了'
}
const tojson = JSON.stringify(arr)
console.log(tojson)//你失真了
在数组中: 存在 Undefined/Symbol/Function 数据类型时会变为 null。 存在 Infinity/NaN 也会变成 null。
const arr = [function () { }, undefined, NaN, Infinity, Symbol(), 2]
const tojson = JSON.stringify(arr)
console.log(tojson)//[null,null,null,null,null,2]
在对象中: 属性值为 Undefined/Symbol/Function 数据类型时,属性和值都不会转为字符串。属性值为Infinity/NaN ,属性值会变为 null。
const obj = {
name: undefined,
age: Symbol(),
sex: function () { },
address: NaN,
friend: Infinity,
hobbit: 'llm'
}
console.log(JSON.stringify(obj))//{"address":null,"friend":null,"hobbit":"llm"}
日期数据类型的值会调用 toISOString。
//日期类型
const time = new Date()
const tojson1 = JSON.stringify(time)
const tojson = time.toISOString()
console.log(tojson1)//"2020-08-26T03:29:20.440Z"
console.log(tojson)//2020-08-26T03:29:20.440Z
非数组/对象/函数/日期的复杂数据类型会变成一个空对象。
const reg = new RegExp("at", 'i')
const res = JSON.stringify(reg)
console.log(res)//{}
JSON.stringify 有第二个参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
function replacer(key, value) {
if (typeof value === "string") {
return undefined;
}
return value;
}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);
console.log(jsonString)// {"week":45,"month":7}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
console.log(JSON.stringify(foo, ['week', 'month']));// {"week":45,"month":7}
循环引用会抛出错误。
何为循环引用?
对象的属性值等于父级的引用。
const obj1 = {
a: null,
name: 'zh'
}
obj1.a = obj1
const res = JSON.stringify(obj1)
console.log(res)//TypeError: Converting circular structure to JSON
实现call方法
核心思路就是把函数作为要绑定对象的一个方法,然后执行函数,最后从绑定对象上删除此方法。
const selfCall = function (context, ...args) {
//这一步等于this,但是this是一个对象呀。
//又误会了这个this了,其实他表示的只是Function类的一个实例,即就是调用的函数而已。
let func = this
context || (context = window)
if (typeof func !== 'function') throw new TypeError('this is not a function')
let caller = Symbol('caller')
context[caller] = func
let res = context[caller](...args)
delete context[caller]
return res
}
Function.prototype.selfCall = selfCall
let name = 'llm'
let obj = {
name: 'zh'
}
function sayName() {
console.log(this.name)
}
sayName.selfCall(obj)
实现apply方法
主要是这里要判断有误参数,然后键传入的数组参数,展开在传入到函数中调用。
function selfApply(context, arr) {
let func = this;
context = context || window;
if (typeof func !== 'function') throw new TypeError('this is not a function')
//唯一的键值
let caller = Symbol('caller')
context[caller] = func
//函数返回值
let res;
if (!arr) {
res = context[caller]()
} else {
res = context[caller](...arr)
}
//删除该函数
delete context[caller]
return res
}
Function.prototype.selfApply = selfApply;
let name = 'llm'
let obj = {
name: 'zh'
}
function sayName(a, b) {
console.log(this.name, a, b)
}
sayName.selfApply(obj, [1, 2])
类型转换
-
让原始值转化为布尔值。只要六种情况是转化为false。
false , undefined , NaN , null , "" , 0
注意,Boolean()不传入任何参数时,会返回false。
-
原始值转化为数字。
如果 Number 函数不传参数,返回 +0,如果有参数,调用
ToNumber(value)
。注意这个
ToNumber
表示的是一个底层规范实现上的方法,并没有直接暴露出来。
参数类型 | 结果 | Undefined | NaN | Null | +0 | Boolean | 如果参数是 true,返回 1。参数为 false,返回 +0 | Number | 返回与之相等的值 | String | 这段比较复杂,看例子 |
---|
其实对于Number转化来说,就是将其传入的参数尽可能的转化为正数或者浮点数。忽略所有的前导0。如果参数中有非数字的字符,那么将会被转化为NaN。但是只有传入的都是空格除外。并且会忽略前导和后导的空格。
Number(" ")//0
Number("123 123")//NaN
Number("123 ")//123
Number(" 123")//123
-
原始值转化为字符串。
直接调用String()方法,不传入参数,则返回一个空字符串。如果有参数,调用
ToString(value)
。这个方法也是底层实现的,没有暴露出来。参数类型 结果 Undefined "undefined" Null "null" Boolean 如果参数是 true,返回 "true"。参数为 false,返回 "false" Number 又是比较复杂,可以看例子 String 返回与之相等的值 -
对象转化为字符串和数字。
toString()方法作用在于返回一个反映这个对象的字符串。
let arr = [1, 2] let arr1 = [] console.log(arr.toString())//"1,2" console.log(arr1.toString())//"" // console.log(null.toString())//报错 // console.log((undefined).toString())//报错 console.log((NaN).toString())//"NaN" let str = "" let str1 = " " let str2 = "zh" console.log(str.toString())//"" console.log(str1.toString())//"" console.log(str2.toString())//"zh" console.log(true.toString())//true console.log(false.toString())//false
而另一个转换对象的函数是 valueOf,表示对象的原始值。默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。
let date = new Date(2020, 8, 27); console.log(date.valueOf()) // 1601136000000
-
ToPrimitive
ToPrimitive(input[, PreferredType])
第一个参数是 input,表示要处理的输入值。 第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。 如果传入的 input 是 Undefined、Null、Boolean、Number、String 类型,直接返回该值。
如果是 ToPrimitive(obj, Number) ,处理步骤如下:
如果 obj 为 基本类型,直接返回 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,JavaScript 抛出一个类型错误异常。
如果是 ToPrimitive(obj, String) ,处理步骤如下:
如果 obj为 基本类型,直接返回 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,JavaScript 抛出一个类型错误异常。
const toPrimitive = (obj, preferredType='Number') => {
let Utils = {
typeOf: function(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
},
isPrimitive: function(obj) {
let types = ['Null', 'String', 'Boolean', 'Undefined', 'Number'];
return types.indexOf(this.typeOf(obj)) !== -1;
}
};
if (Utils.isPrimitive(obj)) {
return obj;
}
preferredType = (preferredType === 'String' || Utils.typeOf(obj) === 'Date') ?
'String' : 'Number';
if (preferredType === 'Number') {
if (Utils.isPrimitive(obj.valueOf())) {
return obj.valueOf()
};
if (Utils.isPrimitive(obj.toString())) {
return obj.toString()
};
} else {
if (Utils.isPrimitive(obj.toString())) {
return obj.toString()
};
if (Utils.isPrimitive(obj.valueOf())) {
return obj.valueOf()
};
}
}
var a={};
ToPrimitive(a);//"[object Object]",与上面文字分析的一致
隐式类型转换
一元运算符(-,+)
他只会正确转换十六进制和十进制,但是不会转化八进制组成的字符串。
console.log(+"0xA")//10
console.log(+"071")//71
console.log(010)//8。这里还是可以转化8进制的数字的,但是不可以转化八进制的字符串
比较(==)
如果两个值分别是数字和字符串/布尔值,其实都是转化为数字,再比较。
x是数字,y是字符串,判断x == ToNumber(y)
x是字符串,y是数字,判断ToNumber(x) == y
x是布尔值,判断ToNumber(x) == y
y是布尔值,判断x ==ToNumber(y)
当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == true
和 xx == false
的写法。
比如:
// 不建议
if (a == true) {}
// 建议
if (a) {}
// 更好
if (!!a) {}
console.log(false == undefined)//false
false == undefined相当于
0 == undefined不符合上面的情形,执行最后一步 返回
false
console.log(false == [])//true
false == []相当于
0 == []相当于
0 == ""相当于
0 == 0,结果返回
true
console.log([] == ![])//true
首先会执行 ![]
操作,转换成 false,相当于 [] == false
相当于 [] == 0
相当于 '' == 0
相当于 0 == 0
,结果返回 true
疑问:为什么![]表示的是false。
解答:github,[]表示的是对象,对象都是true,所以![]为false。
最后再举一些会让人踩坑的例子:
console.log(false == "0")
console.log(false == 0)
console.log(false == "")
console.log("" == 0)
console.log("" == [])
console.log([] == 0)
console.log("" == [null])
console.log(0 == "\n")
console.log([] == 0)
以上均返回 true。
[[][0] + []][0][5]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][8]+[[[] == []][0] + []][0][2]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]]+[]][0][23]+[[][0] + []][0][3]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][8]+[+[1 + [[][0] + []][0][3] +309][0] + []][0][7]+[[][[[][0] + []][0][4]+[[][0] + []][0][5]+[[][0] + []][0][1]+[[][0] + []][0][2]] + []][0][6]+[[][0] + []][0][0]
//i love you
总结:
1. undefined == null,结果是true。且它俩与所有其他值比较的结果都是false。
2. String == Boolean,需要两个操作数同时转为Number。
3. String/Boolean == Number,需要String/Boolean转为Number。
4. Object == Primitive,需要Object转为Primitive(具体通过valueOf和toString方法)。
当我们用 Number() 等函数的时候,就是显式类型转换,其转换规则是当是基本类型时,参照规范中的对应表进行转换,当不是基本类型的时候,先参照规范中的 ToPrimitive
方法转换为基本类型,再按照对应表转换,当执行 ToPrimitive 的时候,又会根据情况不同,判断先执行对象的 valueOf 方法还是 toString 方法进行准换。
强弱类型的判断
按照计算机语言的类型系统的设计方式,可以分为强类型和弱类型两种。二者之间的区别,就在于计算时是否可以不同类型之间对使用者透明地隐式转换。从使用者的角度来看,如果一个语言可以隐式转换它的所有类型,那么它的变量、表达式等在参与运算时,即使类型不正确,也能通过隐式转换来得到正确地类型,这对使用者而言,就好像所有类型都能进行所有运算一样,所以这样的语言被称作弱类型。与此相对,强类型语言的类型之间不一定有隐式转换。
js自动插入分号规则
首先这些规则是基于两点:
- 以换行为基础;
- 解析器会尽量将新行并入当前行,当且仅当符合ASI规则时才会将新行视为独立的语句。
ASI规则
1. 新行并入当前行将构成非法语句,自动插入分号。
if(1 < 10) a = 1
console.log(a)
// 等价于
if(1 < 10) a = 1;
console.log(a);
2. 在continue,return,break,throw后自动插入分号。
3. ++、--后缀表达式作为新行的开始,在行首自动插入分号。
4. 代码块的最后一个语句会自动插入分号。
No ASI规则:
1. 新行以 ( 开始
2. 新行以 [ 开始
3. 新行以 / 开始
4. 新行以 + 、 - 、 % 和 * 开始
5. 新行以 , 或 . 开始
{}的两种解读
①当{}的前面有运算符号的时候,+,-,*,/,()等等,{}都会被解析成对象字面量,这无可争议。 ②当{}前面没有运算符时候但有;结尾的时候,或者浏览器的自动分号插入机制给{}后面插入分号(;)时候,此时{}都会被解析成代码块。 ③如果{}前面什么运算符都没有,{}后面也没有分号(;)结尾,Firefox会始终如一的解析为代码块,而chrome有细微的差别,chrome会解析为对象字面量。
运算符的优先级
ECMAScript 运算符优先级
运算符 | 描述 | . [] () | 字段访问、数组下标、函数调用以及表达式分组 | ++ -- - + ~ ! delete new typeof void | 一元运算符、返回数据类型、对象创建、未定义值 | * / % | 乘法、除法、取模 | + - + | 加法、减法、字符串连接 | << >> >>> | 移位 | < <= > >= instanceof | 小于、小于等于、大于、大于等于、instanceof | == != === !== | 等于、不等于、严格相等、非严格相等 | & | 按位与 | ^ | 按位异或 | && | 逻辑与 | ?: | 条件 | = oP= | 赋值、运算赋值 | , | 多重求值 |
---|
自增的是引用,而非值。
++[]//报错
let a = []
++a//1
分析一些列子
1. {}+{}//chrome:"[object Object][object Object]",Firfox:NaN
2. {}+[]//0
3. []+{}//"[object Object]"
就是矛盾在于{}是看成代码块还是对象字面量。
Firefox始终如一,第一个{}一直解析为代码块,运算符号后面{}解析为对象字面量。
但是google有时候解析成代码块,有时候解析成对象字面量。 但是我们可以这样记住:Chrome对于首是"{"尾是"}"的表达式自动添加了括号。
为什么对象值放在堆中,基本数据类型放在栈中?
能量是守衡的,无非是时间换空间,空间换时间的问题 堆比栈大,栈比堆的运算速度快,对象是一个复杂的结构,并且可以自由扩展,如:数组可以无限扩充,对象可以自由添加属性。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。相对于简单数据类型而言,简单数据类型就比较稳定,并且它只占据很小的内存。不将简单数据类型放在堆是因为通过引用到堆中查找实际对象是要花费时间的,而这个综合成本远大于直接从栈中取得实际值的成本。所以简单数据类型的值直接存放在栈中。
防抖和节流
apply第二个参数可以是一个数组也可以是一个类数组。
函数防抖:
-
将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
// 2、防抖功能函数,接受传参
function debounce(fn, delay=1000) {
// 4、创建一个标记用来存放定时器的返回值
let timeout = null;
return function() {
// 5、每次当用户点击/输入的时候,把前一个定时器清除
clearTimeout(timeout);
// 6、然后创建一个新的 setTimeout,
// 这样就能保证点击按钮后的 interval 间隔内
// 如果用户还点击了的话,就不会执行 fn 函数
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
函数节流:
-
使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
// 2、节流函数体
function throttle(fn, delay=1000) {
// 4、通过闭包保存一个标记
let canRun = true;
return function() {
// 5、在函数开头判断标志是否为 true,不为 true 则中断函数
if(!canRun) {
return;
}
// 6、将 canRun 设置为 false,防止执行之前再被执行
canRun = false;
// 7、定时器
setTimeout( () => {
fn.apply(this, arguments);
// 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
canRun = true;
}, delay);
};
}
区别:
-
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
-
主要是对返回函数的处理。节流是设置开关为false。如果执行到规定的时间。将开关设置为true。利用if函数判断开关。返回空值。不让其继续执行。等到规定时间后会继续执行。防抖是先清除延时器,然后再设置延时器调用函数。
浏览器的缓存机制
强缓存和协商缓存最大也是最根本的区别是:强缓存命中的话不会发请求到服务器(比如chrome中的200 from memory cache),协商缓存一定会发请求到服务器,通过资源的请求首部字段验证资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的实体,而是通知客户端可以从缓存中加载这个资源(304 not modified)。
浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中,用于更新上次缓存的结果。
限制缓存时间
- Cache-Control: max-age, 单位是秒。是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。
- Expires(期满,终止)是一个绝对时间。用以表达在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求
- Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。 如果同时存在则使用Cache-control。
Cache-Control的优先级比expires高
强制缓存
强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。
缓存最后到达两个位置中。利用size做标识。
- from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk。
在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。
协商缓存
协商缓存就是强制缓存失效后, 浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。
协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。
- Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间。
- Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;
- Etag/If-None-match表示的是服务器资源的唯一标识,只要资源变化,Etag就会重新生成。
If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件。(就是当上次从服务器返回的最后修改时间小于再次请求时服务器对该文件的修改时间,就重新请求文件。 )
- Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。这个可以解决Last-Modified 的时间精度和准确度问题
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生效。
总结:强制缓存优先于协商缓存。协商缓存成功则返回304状态码,继续使用缓存。否则返回200状态码,重新请求资源。HTTP 缓存可以分为强制缓存和协商缓存,强制缓存就是在缓存有效期内直接使用浏览器缓存;协商缓存则需要先询问服务端资源是否发生改变,如果未改变再使用浏览器缓存。
数组乱序
// 方法一。该方法有缺陷,大多数元素的位置是不变的
function mixArr(arr) {
return arr.sort(() => Math.random() - 0.5)
}
// 方法二。 洗牌算法
function shunflee(arr) {
const len = arr.length
while(len > 1) {
const index = parsetInt(Math.random() * len--)
[arr[index], arr[len]] = [arr[len], arr[index]]
}
return arr
}
数组去重
// 方法一。 利用对象的键不能重复的特点
function removeDup(arr) {
const obj = {}
arr.forEach((item) => {
if(!obj[item]) {
obj[item] = true
}
})
return Object.keys(obj)
}
// 方法二。 set数据结构
Array.from(new Set(arr))
[...new Set(arr)]
new 操作符作用和实现
1.创建一个对象
2.绑定prototype
3.改变this指向
4.返回新的对象
function MyNew(Constructor, ...arg) {
// 1.创建一个对象
const newObj = {}
// 2.绑定prototype
newObj.__proto__ = Constructor.prototype
// 3.改变this指向
Constructor.call(newObj, ...arg)
// 4.返回新的对象
return newObj
}
数组的sort方法
let arr = [1, 22, 11, 332, 42, 12, 222]
arr.sort((a, b) => {
//前一个参数是数组的后一项,第二个参数是数组的前一项
// console.log(a, b)
// 升序是按顺序相减,降序是反向相减。
return a - b
})
// 原数组被改变。
console.log(arr)
字符串去重
const str = [...new Set("zhhsajwnns")].join("");
闭包
内部函数引用外部函数的变量,他会单独放在一个活动对象中,如果外部对象执行完毕,他的内存空间会被释放,但是内部函数所引用的那个活动对象不会被释放。
优点
- 可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用
- 避免变量污染全局
- 把变量存到独立的作用域,作为私有成员存在
缺点
- 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏
- 对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
- 可能获取到意外的值(captured value)
类数组和数组
转化:转换后数组的长度会有length决定。索引不连续时转换结果是连续的,会自动补位。
-
Array.from():不连续的索引,设置为undefined
// 代码示例 let al2 = { length: 4, '-1': -1, '0': 0, a: 'a', 1: 1 }; console.log(Array.from(al2)); // [0, 1, undefined, undefined]
-
Array.prototype.slice.call():生成的是稀疏数组。
// 代码示例 let al2 = { length: 4, '-1': -1, '0': 0, a: 'a', 1: 1 }; console.log(Array.prototype.slice.call(al2)); //[0, 1, empty × 2]
内存泄漏
- 闭包使用不当引起内存泄漏
- 全局变量(js不会主动回收全局变量中的垃圾)
- 分离的DOM节点
- 控制台的打印
- 遗忘的定时器
执行上下文对象
变量对象
作用域链
this
通过底层思想判断this指向
1.计算 MemberExpression (函数括号左边的部分)的结果赋值给 ref
2.判断 ref 是不是一个 Reference 类型
Reference 的构成,由三个组成部分,分别是:
- base value:属性所在的对象或者就是 EnvironmentRecord
- referenced name: 变量名称
- strict reference
3.如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
- IsPropertyReference判断是否为true?如果base value为一个对象,他就是true。
如果 ref 不是Reference,那么 this 的值为 undefined。在严格模式下,在非严格模式下是window
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!