JS面试—作用域与闭包

目录

一、题目

1、说一下对变量提升的理解

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

3、创建10个标签。点击的时候弹出来对应的序号

4、如何理解作用域

5、实际开发中闭包的应用

二、知识点

2.1 执行上下文

2.2 this

2.3 作用域

2.4 作用域链

2.5 闭包


一、题目

1、说一下对变量提升的理解

  • 变量的定义
  • 函数的声明(注意和函数表达式的区别)

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

作为普通函数执行     (默认绑定)

作为对象属性执行    (隐式绑定)

call、apply、bind(强绑定)    (显示绑定)

作为构造函数         (new绑定)

优先级:new绑定>显示绑定>隐式绑定>默认绑定 

3、创建10个<a>标签。点击的时候弹出来对应的序号

var i
for(i=0;i<10;i++){ 
    (function(){   //自执行函数  不用调用,只要定义完成,立即执行
        var a=document.createElement('a');
        a.innerHTML=i+'<br>'
        a.addEventListener('click',function(){
            e.preventDefault();
            alert(i)   //自由变量  去父级作用域寻找
        })
        document.body.appendChild(a)
    })(i)
}

错误写法,每次点击输出的都是10,因为点击的时候循环已经结束,页面上已经有0,1,2,3,4,5,6,7,8,9,此时i==10

var i,a
for(i=0;i<10;i++){   //全局作用域 会覆盖
        a=document.createElement('a');
        a.innerHTML=i+'<br>'
        a.addEventListener('click',function(){
            e.preventDefault();
            alert(i)
        })
        document.body.appendChild(a)
}

俩种方法实现创建10个li标签,点击显示对应的i 

//创建10个li  点击显示对应的i
function add(){
    var ul=document.getElementById("ul");
    var str=""
    for(var i=0;i<10;i++){
           str +="<li>li</li>"
    }
    ul.innerHTML=str

    // var li=document.getElementsByTagName("li")
    // var liLength=li.length
    // for(let i=0;i<liLength;i++){
    //     li[i].onclick=function (){
    //         alert(i)
    //     }
    // }

    ul.onclick=function (event){
        var target = event.target || event.srcElement;
        var li=document.getElementsByTagName("li")
        var liLength=li.length
        if(target.nodeName.toLowerCase() == "li"){
            for(var i=0;i<liLength;i++){
                if(li[i]===target){
                    alert(i)
                }
            }
        }
    }
}

add()

4、如何理解作用域

  • 自由变量
  • 作用域链,即自由变量的查找
  • 闭包的俩个场景

5、实际开发中闭包的应用

闭包在实际应用中主要用于封装变量,收敛权限。

function isFirstLoad(){
var _list=[];   //变量前加下划线  表示变量是私有的
return function(id){
if(_list.indexOf(id)>=0){
return false;
}else{
_list.push(id);
return true;
}
}
}

//使用
var firstLoad=isFirstLoad();
firstLoad(10);//true
firstLoad(10);//false
firstLoad(20);//true
firstLoad(20);//false

在isFirstLoad函数外面无法修改 _list的值

二、知识点

PS:代码的注释也很重要哦   一定要阅读,有知识点的讲解

2.1 执行上下文

  • 范围:一段<script>或者一个函数声明,都是执行上下文环境。
  • 全局:在一段脚本代码中,变量定义、函数声明前置。
  • 函数:在一段函数中,变量定义、函数声明、this、arguments前置。
console.log(a);  //undefined   占位
var a=100;   
//前两行代码运行的时候会把变量提前,提前之后代码相当于:

var a=undefined;
console.log(a);
a=100;

//当console.log输出a的时候,a还没有赋值,所以输出的是undefined

fn(zhangsan);   //zhangsan 20
function fn (name){
    console.log(this)
    console.log(arguments)
    age=20;
    console.log(name,age)
    var age
}
//上面六行代码运行的时候会把函数声明提前,函数里面会把变量提前,提前之后代码相当于:
function fn(name){
var age;
age=20;
console.log(name,age);
}
fn("zhangsan");
//当console.log输出name和age的时候,name参数和age都进行赋值了,所以输出的是"zhangsan" 20。

注意:变量前置的时候只能把变量名称前置,函数声明前置的时候是整个函数前置。 

PS:注意“函数声明”和“函数表达式”的区别

  函数声明在代码运行的时候前置,函数表达式不前置。

fn()  //正确
function fn (){
    //函数声明
}
fn1()  //错误 fn1相当于一个变量
var fn1=function(){
    //函数表达式
}

2.2 this

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

也就是说,一个对象或者函数你只定义了,没有执行,this永远都不知道它指向谁。记住最核心的一句话:哪个对象调用函数,函数里面的this指向哪个对象。

var a={
    name:'A',
    fn:function (){
        console.log(this.name)
    }
};
a.fn();  //当fn作为一个对象的属性执行的时候,this指向对象a本身,即this===a   //作为对象属性执行
a.fn.call({name:'B'});  //当用call转移对象之后,this指向转移后的对象this==={name:"B"}
var fn1 =a.fn;
fn1();  //当在一个普通函数中执行时,this指向的是window即this===window     //普通函数执行
  • this的使用场景 

1、作为构造函数执行    (构造函数中生成一个隐式的this引用变量,来记录当前这个构造函数是被哪一个对象调用。)

function Foo(name){
this.name=name;
//相当于这里有个this空对象,然后给对象加属性,然后再返回this
//this={};
//this.name=name;
//return this;
}
var f=new Foo("zhangsan");
console.log(f.name)   //zhangsan

用new来调用Foo()时,我们会构造一个新对象f并把它绑定到Foo()调用中的this上 

回忆下构造函数运行的几个步骤

1、实例化对象,一个新对象被创建,新对象的隐式原型指向构造函数的显示原型

2、构造函数被执行,同时this被指定为新实例

3、如果构造函数返回一个对象,这个对象会取代整个new出来的结果,如果构造函数没有返回对象,new出来的结果就是1创建的对象),就能得出正确的答案了。

注意:构造函数有一个坑,这里必要提一下,在构造函数里面返回一个对象,this会指向返回的这个对象,而不是执行构造函数后创建的对象,这个,回忆下构造函数运行的步骤,也能得出正确的答案。

 2、作为对象属性执行

var obj ={
    name:"A",
    printName:function (){
        console.log(this.name)
    }
};
obj.printName(); 
//把函数作为对象属性执行,this就指向的是这个对象
//很明显,上面代码输出的结果是A,调用函数的是obj,因此this指向obj。

虽然obj1的fn是从obj2的fn赋值而来,但是调用函数的是obj1,所以this指向obj1。 

3、作为普通函数执行      函数体是严格模式this会被绑定到undefined,否则就是指向全局对象window

function fn(){
    console.log(this)   //this===window
}
fn()  

4、call  apply  bind 

简单来说,call和apply有三个主要用途,1、改变this的指向,this从obj2指向了obj1。2、借用方法,obj1没有fn方法,只是借用obj2的方法。3、根据这个this和call还可以实现继承。

call和apply的作用,完全一样,唯一的区别就是在参数上面,call接收的参数不规定,第一个参数是函数体内this的指向,第二个参数以后是依次传入的参数。apply接收两个参数,第一个参数也是函数体内this的指向。第二个参数是一个集合对象(数组或者类数组) 

function fn1(name,age){
    alert(name)
    console.log(this)
}
fn1.call({x:100},'zhangsan',20)
//用call转移上下文,这个this就是指向第一个参数{x:100},
//这个写法第一个参数表示this,第二个参数就是函数里传递的参数,如果有第三个继续往后写
n1. apply({x:100},["zhangsan",20]);
//apply作用一样,写法跟call稍有区别,就是传递参数的方式不一样,它把函数里的参数放在数组里传递

 bind的写法跟上面两个写法不太一样,用bind的时候必须是一个函数表达式,它是在函数的后面直接写上bind,然后再调用函数

var fn2=function (name,age){
    alert (name)
    console.log(this)
}.bind({y:200})
fn2('zhangsan',20)

注意:

  1. 在jquery一些常用框架脚本里最常用的是用call改变this的值。 
  2. 如果把null或者undefined作为this的绑定对象传入call,apply或者bind,这些值调用时会被忽略,实际应用的是默认绑定规则
  3. bind不能与new绑定同时使用

5、箭头函数

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this

箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this

function foo(){
//返回一个箭头函数
     return (a)=>{
        //this 继承自foo()
        console.log(this.a)
     };
}
var obj1={
   a:2
};
var obj2={
   a:3
};
var bar =foo.call(obj1);  
bar.call(obj2) //2  不是3    

foo()内部创建的箭头函数会捕获调用时foo()的this。 箭头函数的绑定不会被修改

箭头函数最常用于回调函数中,例如事件处理器或者定时器

 function foo(){
        setTimeout(()=>{
           console.log(this.a)
        },100)
    }
    var obj={
        a:2
    };
foo.call(obj);   //a

2.3 作用域

  • JS没有块级作用域  (ES6中有块级作用域)
if(true){
    var name='zhangsan'     //因为js没有块级作用域,所以这一行也可以看成是name声明在if上面
}
console.log(name);   
//在java等高级语言中会报错,变量会受块级作用域的影响,在js中没有块级作用域
//js中为了方便阅读,不建议这样写,这样写容易使程序不易读,建议写成下面这种方式:

var name;
if(true){
name="zhangsan";
}
console.log(name);
  • 只有函数和全局作用域
var a=100   //全局作用域
function fn () {   //函数作用域
    var a = 200             //定义在函数里,外面不能修改也不能使用
    console.log('fn', a) //200
}
console.log('global',a) //100
fn()

2.4 作用域链

  • 自由变量:当前作用域没有定义的变量 ,只关注在哪被定义,不关注在哪被使用
var a=100 ;
function fn (){
    var b=200;
    console.log(a); 
//当前作用域(即函数作用域)没有定义a变量,然后就去函数作用域的父级作用域中寻找a,这个a就属于一个“自由变量”
    console.log(b);
}
fn()

注意:函数的父级作用域,不是函数调用时候的父级而是函数声明(或者函数定义)时候的父级作用域。 

  • 什么叫作用域链(即自由变量的查找)?

就是一个自由变量一个自由变量一直不断的往父级作用域找,就是作用域链

var a=100 ;
function F1 (){
    var b=200;
    function F2(){
        var c=300
        console.log(a);  //自由变量,a向上找,再向上找,就是个作用域链
        console.log(b);  //自由变量
        console.log(c);
    }
F2()
}
F1()

2.5 闭包

理解闭包 参考本文章http://blog.sina.com.cn/s/blog_c112a2980102xl89.html

  •  闭包的使用场景

函数作为返回值

function F1(){
    var a=100
    return function (){
        console.log(a)  //自由变量  父作用域中查找
    }
}
//f1 得到一个函数
var  f1 =F1()
var  a=200;
f1();   //100

函数作为参数传递

function F1(){
    var a=100
    return function (){
        console.log(a)
    }
}
var  f1 =F1()

function F2(fn){
    var a=200
    fn()
}
F2(f1)  //100
  • 闭包的作用

避免全局变量暴露在外面(可以读取函数内部的变量),造成全局变量的污染。(在函数内部封装变量)

这些变量的值始终保存在内存中。

  • 使用闭包的注意点

1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题,在IE中可能导致内存泄漏。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的共用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值