高级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 闭包方案的主要内容,如果未能解决你的问题,请参考以下文章

解析js中作用域闭包——从一道经典的面试题开始

js面试题知识点全解(一作用域和闭包)

JS面试题(进阶)——原型链、this指向、闭包

闭包的特点&&闭包面试题

js 闭包(面试题)

Javascript中关于作用域和闭包和域解释的面试题