资讯专栏INFORMATION COLUMN

【前端面试】作用域和闭包

yanest / 1662人阅读

摘要:作用域没有块级作用域尽量不要在块中声明变量。只有函数级作用域作用域链自由变量当前作用域没有定义的变量即为自由变量。自由变量会去其父级作用域找。

1. 题目

说一下对变量提升的理解

说明this的几种不同使用场景

创建10个a标签,点击的时候弹出来相应的序号

如何理解作用域

实际开发中闭包的应用

手动实现call apply bind

2. 知识点 2.1 执行上下文

范围:一段script或者一个函数

全局:变量定义、函数声明 script

函数:变量定义、函数声明、this、arguments (执行之前)

函数声明和函数表达式的区别:

a(); //报错  函数表达式 变量声明 会提前。
var a = function(){}

b(); // 不报错  函数声明
function b(){}

变量定义时会默认把他的变量声明提升:(仅限于他的执行上下文,比如一段script和一个函数中)

console.log(a);
var a = 0;

实际上是

var a;
console.log(a);
a = 0;
2.2 this

this要在执行时才能确认,定义时无法确认。

        var a = {
            name:"a",
            fn:function(){
                console.log(this.name);
            }
        }

        a.fn();  // a
        a.fn.apply({name:"b"});  // b  a.fn.call({name:"b"});
        var fn1 = a.fn();
        fn1();  // undefined

this的使用场景

构造函数中(指向构造的对象)

    function Fun(name){
        this.name = name;
    }
    var f = new Fun("a");
    console.log(f.name);

对象属性中(指向该对象)

普通函数中(指向window)

call apply bind

都是用来改变一个函数的this指向,用法略有不同。

call:后面的参数为调用函数的参数列表

function greet(name) {
  console.log(this.animal,name);
}

var obj = {
  animal: "cats"
};

greet.call(obj,"猫咪");

apply:第二个参数为调用函数的参数数组

function greet(name) {
  console.log(this.animal,name);
}

var obj = {
  animal: "cats"
};

greet.apply(obj,["猫咪"]);

bind:当绑定函数被调用时,bind传入的参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。

var fun = function (name1,name2){
    console.log(this);
    console.log(name);
}.bind({a:1},"name1");
    fun("name2");

arguments中的this:

var length = 10;
function fn(){
    alert(this.length)
}
var obj = {
    length: 5,
    method: function(fn) {
        arguments[0]()
    }
}

obj.method(fn)//输出1
这里没有输出5,也没有输出10,反而输出了1,有趣。这里arguments是javascript的一个内置对象(可以参见mdn:arguments - JavaScript),是一个类数组(就是长的比较像数组,但是欠缺一些数组的方法,可以用slice.call转换,具体参见上面的链接),其存储的是函数的参数。也就是说,这里arguments[0]指代的就是你method函数的第一个参数:fn,所以arguments[0]()的意思就是:fn()。

不过这里有个疑问,为何这里没有输出5呢?我method里面用this,不应该指向obj么,至少也会输出10呀,这个1是闹哪样?

实际上,这个1就是arguments.length,也就是本函数参数的个数。为啥这里的this指向了arguments呢?因为在Javascript里,数组只不过使用数字做属性名的方法,也就是说:arguments[0]()的意思,和arguments.0()的意思差不多(当然这么写是不允许的),你更可以这么理解:

arguments = {
    0: fn, //也就是 functon() {alert(this.length)} 
    1: 第二个参数, //没有 
    2: 第三个参数, //没有
    ..., 
    length: 1 //只有一个参数
}

所以这里alert出来的结果是1。

如果要输出5应该咋写呢?直接 method: fn 就行了。

2.3 作用域

没有块级作用域

        if(true){
            var name = "test"
        }
        console.log(name);

尽量不要在块中声明变量。

只有函数级作用域

2.4 作用域链

自由变量 当前作用域没有定义的变量 即为自由变量。

自由变量会去其父级作用域找。是定义时的父级作用域,而不是执行。

        var a = 100;
        function f1(){
            var b = 200;
            function f2(){
                var c = 300;
                console.log(a); //自由变量
                console.log(b); //自由变量
                console.log(c);
            }
            f2();
        };
        f1();
2.5 闭包

 一个函数中嵌套另外一个函数,并且将这个函数return出去,然后将这个return出来的函数保存到了一个变量中,那么就创建了一个闭包。

闭包的两个使用场景

1.函数作为返回值

        function fun(){
            var a = 0;
            return function(){
                console.log(a); //自由变量,去定义时的父级作用域找
            }
        }

        var f1 = fun();
        a = 1000;
        f1();

2.函数作为参数

        function fun(){
            var a = 0;
            return function(){
                console.log(a); //自由变量,去定义时的父级作用域找
            }
        }

        function fun2(f2){
            a = 10000
            f2();
        }

        var f1 = fun();

        fun2(f1);

具体解释看 高级-闭包中的说明

闭包的两个作用:

能够读取其他函数内部变量的函数

可以让函数内部的变量一直保存在内存中

实际应用场景1:

闭包可以将一些不希望暴露在全局的变量封装成“私有变量”。

假如有一个计算乘积的函数,mult函数接收一些number类型的参数,并返回乘积结果。为了提高函数性能,我们增加缓存机制,将之前计算过的结果缓存起来,下次遇到同样的参数,就可以直接返回结果,而不需要参与运算。这里,存放缓存结果的变量不需要暴露给外界,并且需要在函数运行结束后,仍然保存,所以可以采用闭包。

上代码:

function calculate(param){
    var cache = {};
    return function(){
        if(!cache.parame){
            return cache.param;
        }else{
            //缓存计算....
            //cache.param = result
            //下次访问直接取
        }
    }
}

实际应用场景2

延续局部变量的寿命

img 对象经常用于进行数据上报,如下所示:

var report = function( src ){
    var img = new Image();
    img.src = src;
};
report( "http://xxx.com/getUserInfo" );

但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在 bug,在这些浏览器
下使用 report 函数进行数据上报会丢失 30%左右的数据,也就是说, report 函数并不是每一次
都成功发起了 HTTP 请求。

丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的
调用结束后, img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求
就会丢失掉。

现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:

var report = (function(){
    var imgs = [];
    return function( src ){
        var img = new Image();
        imgs.push( img );
        img.src = src;
    }
})();

闭包缺点:浪费资源!

3. 题目解答 3.1 说一下对变量提升的理解

变量定义和函数声明

注意函数声明和函数表达式的区别

变量定义时会默认把他的变量声明提升:(仅限于他的执行上下文,比如一段script和一个函数中)

console.log(a);
var a = 0;

实际上是

var a;
console.log(a);
a = 0;
3.2 说明this的几种不同使用场景

构造函数中(指向构造的对象)

对象属性中(指向该对象)

普通函数中(指向window)

call apply bind

3.3 创建10个a标签,点击的时候弹出来相应的序号

实现方法1:用let声明i

        var body = document.body;
        console.log(body);
        for (let i = 0; i < 10; i++) {
            let obj = document.createElement("i");
            obj.innerHTML = i + "
"; body.appendChild(obj); obj.addEventListener("click",function(){ alert(i); }) }

实现方法2 包装作用域

    var body = document.body;
    console.log(body);
    for (var i = 0; i < 10; i++) {
        (function (i) {
            var obj = document.createElement("i");
            obj.innerHTML = i + "
"; body.appendChild(obj); obj.addEventListener("click", function () { alert(i); }) })(i) }
3.4 实际开发中闭包的应用

能够读取其他函数内部变量的函数

可以让函数内部的变量一直保存在内存中

封装变量,权限收敛

应用1

var report = (function(){
    var imgs = [];
    return function( src ){
        var img = new Image();
        imgs.push( img );
        img.src = src;
    }
})();

用于防止变量销毁。

应用2

    function isFirstLoad() {
        var arr = [];
        return function (str) {
            if (arr.indexOf(str) >= 0) {
                console.log(false);
            } else {
                arr.push(str);
                console.log(true);
            }
        }
    }

    var fun = isFirstLoad();
    fun(10);
    fun(10);

将arr封装在函数内部,禁止随意修改,防止变量销毁。

3.5 手动实现call apply bind

context 为可选参数,如果不传的话默认上下文为 window;

context 创建一个 Symbol 属性,调用后即删除,不会影响context

    Function.prototype.myCall = function (context) {
      if (typeof this !== "function") {
        return undefined; // 用于防止 Function.prototype.myCall() 直接调用
      }
      context = context || window;
      const fn = Symbol();
      context[fn] = this;
      const args = [...arguments].slice(1);
      const result = context[fn](...args);
      delete context[fn];
      return result;
    }

apply实现类似call,参数为数组

    Function.prototype.myApply = function (context) {
      if (typeof this !== "function") {
        return undefined; // 用于防止 Function.prototype.myCall() 直接调用
      }
      context = context || window;
      const fn = Symbol();
      context[fn] = this;
      let result;
      if (arguments[1] instanceof Array) {
        result = context[fn](...arguments[1]);
      } else {
        result = context[fn]();
      }
      delete context[fn];
      return result;
    }

1.判断是否为构造函数调用

2.注意参数要插入到目标函数的开始位置

    Function.prototype.myBind = function (context) {
      if (typeof this !== "function") {
        throw new TypeError("Error")
      }
      const _this = this
      const args = [...arguments].slice(1)
      return function F() {
        // 判断是否用于构造函数
        if (this instanceof F) {
          return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
      }
    }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101742.html

相关文章

  • 【进阶2-3期】JavaScript深入之闭包面试题解

    摘要:闭包面试题解由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起的一个副作用就是如果内部函数在一个循环中,那么变量的值始终为最后一个值。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本周正式开始前端进阶的第二期,本周的主题是作用域闭包,今天是第8天。 本计划一共28期,每期重点攻克一个面试重难点,如果你还不了...

    alanoddsoff 评论0 收藏0
  • 【进阶2-2期】JavaScript深入之从作用域链理解闭包

    摘要:使用上一篇文章的例子来说明下自由变量进阶期深入浅出图解作用域链和闭包访问外部的今天是今天是其中既不是参数,也不是局部变量,所以是自由变量。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本周正式开始前端进阶的第二期,本周的主题是作用域闭包,今天是第7天。 本计划一共28期,每期重点攻克一个面试重难点,如果你还不了解本进阶计...

    simpleapples 评论0 收藏0
  • 你应该要知道的作用域和闭包

    摘要:写在前面对于一个前端开发者,应该没有不知道作用域的。欺骗词法作用域有两个机制可以欺骗词法作用域和。关于你不知道的的第一部分作用域和闭包已经结束了,但是,更新不会就此止住未完待续 这是《你不知道的JavaScript》的第一部分。 本系列持续更新中,Github 地址请查阅这里。 写在前面 对于一个前端开发者,应该没有不知道作用域的。它是一个既简单有复杂的概念,简单到每行代码都有它的影子...

    JouyPub 评论0 收藏0
  • 深入javascript——作用域和闭包

    摘要:注意由于闭包会额外的附带函数的作用域内部匿名函数携带外部函数的作用域,因此,闭包会比其它函数多占用些内存空间,过度的使用可能会导致内存占用的增加。 作用域和作用域链是javascript中非常重要的特性,对于他们的理解直接关系到对于整个javascript体系的理解,而闭包又是对作用域的延伸,也是在实际开发中经常使用的一个特性,实际上,不仅仅是javascript,在很多语言中都...

    oogh 评论0 收藏0
  • Web前端经典面试试题(一)

    摘要:本篇收录了一些面试中经常会遇到的经典面试题,并且都给出了我在网上收集的答案。网页的行为层负责回答内容应该如何对事件做出反应这一问题。 本篇收录了一些面试中经常会遇到的经典面试题,并且都给出了我在网上收集的答案。眼看新的一年马上就要开始了,相信很多的前端开发者会有一些跳槽的悸动,通过对本篇知识的整理以及经验的总结,希望能帮到更多的前端面试者。(如有错误或更好的答案,欢迎指正,水平有限,望...

    princekin 评论0 收藏0

发表评论

0条评论

yanest

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<