JS 作用域和闭包

作用域

JS作用域的概念:当函数被调用时,会创建一个执行环境和相应的作用域,该执行环境里包含了变量或函数有权访问的数据。
每一个执行环境都有一个变量对象[[Scope]],该对象保存着执行环境中的所有变量和函数,我们的代码无法访问这个对象,JS 解析器在处理数据时会使用到它。
1、全局执行环境
在 JS 中,根据宿主的不同,全局执行环境对象也不同,对于 WEB 而言,全局执行环境是 window 对象。

所有的全局变量和函数都是做为 window 对象的属性和方法创建的。

var name = "zhar";
function say(){
  console.log(name);
}
console.log(name);//zhar
console.log(window.name);//zhar
say();//zhar
window.say();//zhar

2、局部执行环境
每个函数有自己的执行环境。

当执行流进入一个函数时,函数的环境会被推入一个环境栈中。函数执行完成后,栈将环境推出。所以在函数之外是没法访问的,这个环境已经不存在。

3、作用域链
当代码在一个环境中执行时,会创建一个作用域链对象(scope chain)。

作用域链的首位,始终是当前执行环境的变量对象。这是一个包含 arguments 和其它命名参数值的活动对象。第二位是外部函数的活动对象,第三位是外部函数的外部函数的活动对象,......,直到作为作用域终点的全局执行环境。

4、没有块级作用域(函数有局部执行环境)
在 JS 中并没有像强类型语言中的块级作用域,比如在 JAVA 中一对花括号中,是一个块级的作用域。

所以下面代码相当于在全部环境中修改了name

var name = 'zhar'
if(true){
  var name = 'tom';
}
console.log(name);
//按着上面作用域链中描述的情况,name 应该输出为 zhar,但实际情况为 tom,便是因为在 JS 中没有块级作用域的概念
//另外一种块级作用域的典型情况为 for 循环
for(var i=0;i<5;i++){
  //dosomething
}
console.log(i);//5

5、变量提升分为声明式和赋值式

var name = 'zhar';
function say(){
  console.log(name);//undefined
  var name = 'tom';
}
say();

上面的示例代码中就存在着变量提升

  1. 声明式
say();//运行正常
function say(){
  console.log("Hello");
}

此为声明式语句,JS 解析器会将声明式语句提升至当前执行环境的顶端

1.赋值式

say();//运行错误  Uncaught TypeError: say is not a function
var say = function(){
  console.log("Hello");
}
//相当于以下代码
var say = undefined;//故会弹出say is not a function
say = function(){
  console.log("hello");
}

此为赋值式语句,JS 解析器会将赋值式语句提升到当前执行环境的顶端,并且赋值为 undefined

闭包

闭包:我的理解就是外部函数能够读取内部函数的变量。可以看做内部函数与外部函数的一座桥梁。

闭包可以形成独立的空间,永久的保存局部变量。保存中间值的状态
缺点是容易造成内存泄漏,因为局部变量永远不会被回收。内存压力太大

1、创建闭包
通常就是在一个函数内部创建另一个(匿名)函数

function say(){
  var name = "zhar";
  return function(){
    return name+"-30";
  }
}
//获取闭包
var hi = say();//Function
console.log(hi());//zhar-30

上面示例中,第三行代码做为 say 方法的一个内部函数,访问了外部的 name 属性,该函数被返回,在其它地方被调用时,仍然可以访问到 say 方法内的 name 属性

根据前面所学的作用域链的知识,可以得出匿名函数中 包含了外部作用域中的 name 属性的引用,当匿名函数被返回后,它的作用域链包含上级函数(say)的活动对象(name)。这样,当 say 函数执行完成后,其活动对象也并不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。

由于闭包所引用的变量不会被自动销毁的特性,在使用闭包时要非常小心,有可能会引起内存占用过多

闭包常见场景

1.循环添加事件BUG

var lis = document.querySelectorAll("li");
for(var i=0;i<lis.length;i++){
  lis[i].onclick = function(){
    console.log(this.innerHTML,i);
  }
}
//在上面的代码中,innerHTML能够得到正确的值,但 i 并不能,这是由于在 onclick 所指定的匿名函数
中使用了外部环境中的同一个活动对象 "i",当外部环境执行完成后,i 的值是 lis 的长度,所以引用了
所有 i 的对象值都变为了 lis的长度
//修改代码如下:
for (var i = 0; i < lis.length; i++) {
  //给onclick添加了一个自执行函数,执行之后会将返回值添加给
onclick事件,即将里面的函数添加到onclick事件上
    lis[i].onclick = (function(index) {
        return function() {
            console.log(lis[index].innerHTML, index);
        }
    })(i);
}
//修改后的代码将 onclick 指定的匿名函数改为了立即执行,并将外部环境的 i 值做为参数传递给该函数
(参数是做为值传递),而在匿名函数内部,由一个闭包函数来引用 index

//另一种方法(这种方法只是做为一种扩展,与本知识点无关)
for (var i = 0; i < lis.length; i++) {
  //在JS中,可以将lis[i]看做一个对象,毕竟万物皆对象,然后可以给这对象添加index属性index为i
    lis[i].index = i;
    lis[i].onclick = function() {
        console.log(this.innerHTML, this.index);
    };
}

2.使用闭包封装一个完整的对象操纵功能

对于一些封装,开发者并不想对外暴露一些属性或变量,此时,可通过闭包来创建

var PERSON = function() {
    var obj = {
        name: "zhar",
        age: 30,
        address: "北京"
    };
    return {
        get: function(pName) {
            return obj[pName];
        },
        add: function(pName, pVal) {
            obj[pName] = pVal;
        },
        del: function(pName) {
            delete obj[pName];
        },
        update: function(pName, pVal) {
            obj[pName] = pVal;
        }
    }
}();
console.log(PERSON.get("name"));//zhar
PERSON.add("desc","175");
console.log(PERSON.get("desc"));//175
PERSON.del("age");
console.log(PERSON.get("age"));//undefined

作用域和闭包经典题目

function f1() {
    var n = 999;
    nAdd = function() {
        n += 1
    }
    function f2() {
        console.log(n);
    }
    return f2;
}
var result = f1();
result();//999
nAdd();//执行n += 1;
result();//1000
function show(i) {
    var a = i;
    function inner() {
        a = 10;
        console.log(1,a);//10
    }
    inner();
    console.log(2,a);//10
}
var a = 0;
show(5);
console.log(3,a);//0
var too = "old";
if (true) {
    var too = "new";
}
console.log(1,too);//new
function test() {
    var too = "new";
}
test()
console.log(2,too);//new
var i = 0;
function outPut(i) {
    console.log(i)
}
function outer() {
    outPut(i);//0
    function inner() {
        outPut(i);//undefined赋值式变量提升
        var i = 1;
        outPut(i);//1
    }
    inner();
    outPut(i);//0
}
outer();
var a = function(){
    a=2;
    console.log(1111,a);
}
function a(){
    a=1;
    console.log(2222,a);
}
a();//2--->因为声明式变量提升先被置顶,然后再被覆盖:即a--->第二个function--->undefined--->第一个function
a();//a is not a function
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容