高级js4 闭包作用域面试题详解 this 闭包方案
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级js4 闭包作用域面试题详解 this 闭包方案相关的知识,希望对你有一定的参考价值。
严格模式下与非严格模式下的形参与arguments的映射
var a = 4;
function b (x,y,a){
console.log(a) // 3
arguments[2].= 10
console.log(a) //严格模式下是3,非严格模式下是10
}
a = b (1,2,3)
console.log(a) //undefined
- 来看看区别,首先,不管严格或者非严格模式下,函数在执行的时候形成一个上下文,然后初始化作用域链,接着初始化arguments {0:1, 1:2, 2:3, length: 3}一个类数组对象,然后形参赋值,x = 1,y =2 , a =3,
- 区别就在这里,非严格模式下,形参会与arguments的值形成映射关系,如x = 1映射到0:1等等,那么当arguments[2] = 10修改的时候,形参a的值也会被修改为10,而严格模式下没有这种映射,所以严格模式下打印出来的是3,而非严格模式下因为映射关系打印出来的是10
匿名函数具名化
如map的回调函数,函数表达式,以及自执行函数,都是匿名函数,但是可以给他一个名字,使其可以调用自身,如
(function a(x){
if(x===1) return 1;
return x + a(x-1)}
)(10)
a只是用来在函数内部使用,并且不会被外层作用域所读取。函数执行的时候正常创建作用域链,形参赋值,变量提升。。。。,但是有个特殊的点就是,具名化的函数会添加一个私有变量,如上,在函数的上下文中,会声明一个a私有变量,而且这个值不允许修改。
如:
var a = 10
(function a (){
console.log(a) // functionn(){}
a= 20. //静默失败
console.log(a). //function(){}
})()
console.log(a) //10
如上,自执行函数给了一个名字a,他会在函数执行的时候在私有上下文中被创建,且无法重新赋值。外部无法读取。所以第一个a打印的就是函数自身。a=20这个静默失败,再打印a也是函数自身。外层的时候a不会被函数自执行的名字影响,所以也是10。但是如果
var a = 10
(function a (){
console.log(a) // undefeind
var a= 20.
console.log(a). // 20
})()
console.log(a) //10
如上,因为自执行函数的变量权限较低,当当前上下文中有基于let/const/var/function等声明的变量,则以我们声明的变量为主。
惰性思想
能够执行一遍的,坚决不执行两遍。
如
// 获取元素样式
function getStyle(ele, arrt){
if('getComputedStyle' in window){
return window.getComputedStyle(ele, attr)
}
else return ele.currentStyle[attr]
}
getStyle('box', 'color')
getStyle('box', 'display')
getStyle('box', 'fontSize')
如上,用来获取一个元素的样式,上诉的方法虽说可以实现我们的需求,但是每次执行都会判断getComputedStyle in window,第一次是必要的,后续每次都有点多余。改造一下
let getClass = function(ele, attr){
//重构getClass
if('getComputedStyle' in window){
getClass = function(ele, attr){
return window.getComputedStyle(ele, attr)
}
} else {
getClass = function(ele, attr){
return ele.currentStyle[attr]
}
}
//首次执行需要返回结果
return getClass(ele, attr)
}
getClass('box', 'display')
getClass('box', 'color')
getClass('box', 'fontSize')
如上,通过改造getClass函数,第一次执行就判断改造,到第二次,第三次的时候就是改造的函数,不用在判断了。
this
不管在哪里执行函数,只要前面没有.,或者没有call back这些,都算是window调用。
- 普通函数的this跟在哪创建,在哪执行无关。
闭包方案
for(var i = 0;i<5;i++){
setTimeout(()=>{
console.log(i)
},1000)
}
如上,我们想在1s后分别输出01234,但是结果却是55555
因为setTimeout中的函数执行的时候,他的上下文没有i这个变量,所以去全局找,此时全局的i已经是5了,所以打印了55555。
解决办法:
for(var i = 0;i<5;i++){
(function(i){
setTimeout(()=>{
console.log(i)
},1000)
})(i)
}
最常见的立即执行函数,函数执行的时候私有变量i被setTimoeut所占用,形成闭包。每轮执行都会创建执行一遍函数,形成了五个上下文,他们的i的值分别是01234,所以最后运行的时候会分别去对应的上下文中拿到i,也就是01234。
也可以:
for(var i = 0;i<5;i++){
setTimeout((function(i){
return function(i){
console.log(i);
}
}(i)),1000)
}
效果跟原理是一样的,内层的函数依赖了iife的上下文的i。形成必报。
也可以:
const fn = (i) => {
return ()=>{
console.log(i);
}
}
for(var i = 0;i<5;i++){
setTimeout(fn(i),1000)
}
预先缓存i值,采用柯里化的思想,原理也是闭包。
- 以上的解决办法都属于命令式编程-自己写逻辑,把控代码的步骤,灵活但是代码繁琐
- 函数式编程解决办法:
[1,2,3,4].forEach((item, index)=>{
setTimeout(()=>{
console.log(index);
},1000)
})
函数式编程-将循环封装成函数,进行调用,无需知道怎么实现,只需在回调函数中写自己的逻辑,开发效率高,但是不够灵活。
- let解决办法:
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
这块代码执行的时候,会产生一个父级上下文(变量i(用来作为判断循环是否执行的关键),跟私有上下文关联。),用来管理循环,每次循环里的代码执行的时候,由于let的原因,会产生一个私有上下文,变量为i。这样五个循环一共会产生五个私有上下文,变量都是i。而且存储的分别是01234,所以最后打印的时候的去对应的私有上下文中获取i。
- 父级上下文跟私有上下文有关联是指:
如
for (let i = 0; i < 5; i++) {
i++
setTimeout(() => {
console.log(i)
}, 1000)
}
结果是135。
- 为什么只执行了三次,不是说有五个私有上下文吗?
原因就是父级上下文。
首先会创建父级上下文,赋值私有变量i=0
然后第一轮循环,产生第一个私有上下文,赋值变量 i = 0
这个私有上下文的i跟父级上下文的是有关联的,在私有上下文里执行了i++变成了1。
此时父级上下文的i也为1。然后因为父级上下文的i是用来判断循环的,还会执行for(i++)中的i++,变为2,而第一个上下文的i还是1,所以第一个打印了1。 - 以此类推,第二个私有上下文创建的时候,i=2,执行i++,i=3,此时父级,私有上下文的i都是3,打印出来的就是3。接着要执行下一个循环了,执行i++,父级上下文i=4。满足条件,进去最后一个循环。以此类推到最后打印出来就是135。
柯里化
柯里化就是利用必报的保存机制,将一些值存储起来,供下级上下文使用。
看一道题
const curring = () => {
//实现方式
}
let add = curring()
let res = add(1)(2)(3)
console.log(res); //6
add = curring()
res =add(1,2,3)(4)
console.log(res); // 10
add = curring()
res = add(1)(2)(3)(4)
console.log(res); //10
求curring的实现方式。curring是利用了柯里化,返回一个函数,而add也是利用柯里化返回一个函数,那么add()执行后怎么打印出值呢?
- 因为:基于console.log(现在试了好像不行),和alert数除以恶函数的时候,会被浏览器转化为字符串(Symbol.toPremitive -> valueof -> toStirng),然后再打印出来,这时候我们只需要拦截一下toPremitive方法,就能实现这种效果。如:
console.log现在行不通
还是会走Symbol.toPrimertive,但是不会将返回的值作为打印的值。
基于这个思路我们就可以实现了
如图,curring返回了add,执行add继续返回自身的add。关键实现就是这个params。使用闭包的保存机制保存参数,改写add的Symbok.toPirmitive让他返回params的和即可。
如图:
重新执行curring的时候会重置params。
这样就能达成我们的目的。
函数组合
编程中还有一个重要的概念,让处理数据的函数像管道一样连接起来,让数据穿过管道实现最终的结果
如
const a = x => x+1
const b = x => x*2
const c = x => x*4
c(b(a(1)))//这样写未免太过难看。
我门创建一个compose函数,他用来接受多个函数为入参(这些函数都只接受一个参数),然后compose返回的也是一个函数如`
comst d = compose(a,b,c)
d(1) === c(b(a(1))) // true
实现这个compose函数:
采用柯里化,闭包的保存机制保存了compose传入的每个函数,然后最后需要执行的时候再遍历执行。
效果是一样的。
补充:
实现简易reduce
以上是关于高级js4 闭包作用域面试题详解 this 闭包方案的主要内容,如果未能解决你的问题,请参考以下文章