JS闭包和作用域

此文首发于 https://lijing0906.github.io
今天来啃闭包和作用域这块难啃的骨头。

什么是闭包

闭包是函数和声明该函数的词法环境的组合。 ----MDN

闭包就是指有权访问另一个函数作用域中的变量的函数。 ----红宝书

什么是作用域

作用域是一个变量和函数的作用范围,JS中函数内声明的所有变量在函数体内始终是可见的,在ES6前有全局作用域和局部作用域,但是没有块级作用域(catch只在其内部生效),局部变量的优先级高于全局变量。

作用域链

Javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。

作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError。

作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。

闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。

闭包的特性

  1. 函数嵌套函数
  2. 内部函数可以访问外部作用域(或外部函数)的变量和参数
  3. 参数和变量不会被回收机制回收,一直存在于内存中,除非手动清除

为什么要用闭包

  1. 希望变量长期存在内存中
  2. 避免全局变量污染

闭包举例及应用

最简单的闭包

function outer() {
    var a = 1;
    return function() {
        return a; // 内部函数访问外部作用域的变量a
    }
}
var b = outer(); // 这一句执行完,变量a并没有被回收,因为要内部函数还需要引用
console.log(b()); // 1 执行内部函数,引用外部变量a

这是最简单的一种闭包,已经包含了闭包的三个特性。

释放对闭包的引用

function makeAdder() {
    return function(x) {
        return x + y;
    }
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
// 释放对闭包的引用
add5 = null;
add10 = null;

上面的代码中,add5和add10都是闭包,它们共享相同的函数定义,但是保存了不同的环境,add5的环境中x是5,add10的环境中x是10。最后都通过null释放了对内部函数(闭包)的引用。

在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收;如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。” ——《JavaScript权威指南》

var name = 'Jane';
function getName () {
  console.log(name);
}
function myName () {
  var name = 'hahaha';
  getName();
}
myName(); // Jane

闭包与for循环

function arrFunc() {
    var arr = [];
    for (var i=0; i<10; i++) {
        arr[i] = function() {
            return i;
        }
    }
    return arr; // 函数声明的时候会把for循环执行一遍,最终i的值时10,后续函数被调用的时候传入的i都是10
}
console.log(arrFunc()[2]()); // 10,arrFunc()执行的结果是arr中有10个匿名函数,每个匿名函数执行的结果都是10

解决办法:

  1. 用闭包
function arrFunc() {
    var arr = [];
    for (var i=0; i<10; i++) {
        arr[i] = function(num) {
            return function() {
                return num;
            }
        }(i);
    }
    return arr;
}
console.log(arrFunc()[2]()); // 2
function arrFunc() {
    var arr = [];
    for (var i=0; i<10; i++) {
        (function(i) {
            arr[i] = function() {
                return i;
            }
        })(i);
    }
    return arr;
}
console.log(arrFunc()[2]()); // 2

因为用了闭包,因此每一次循环都是独立的一个环境,对应不同的i的值,因此才能实现arr数组中是10个返回不同值的匿名函数。

这种闭包可以用于循环添加DOM事件。

  1. 用ES6的let
function arrFunc() {
    var arr = [];
    for (let i=0; i<10; i++) {
        arr[i] = function() {
            return i;
        }
    }
    return arr;
}
console.log(arrFunc()[2]()); // 2

闭包中的this

var obj = {
    name: 'object',
    getName: function() {
        return function() {
            return this.name;
        }
    }
}
console.log(obj.getName()()); // ''

obj.getName()()分成两步来看,第一步执行obj.getName()得到匿名函数function(){return this.name},第二步在全局作用域中执行function(){return this.name}()this指向window对象,它的name属性的值默认是为空的,因此打印空字符串。

想让this指向obj,可以用一下方法:

var obj = {
    name: 'object',
    getName: function() {
        var that = this; // 把this保存起来
        return function() {
            return that.name; // 返回that.name
        }
    }
}
console.log(obj.getName()()); // object

下面这种情况也要注意:

var name = 'window';
var obj = {
    name: 'object',
    getName: function() {
        return this.name;
    }
}
console.log(obj.getName()); // object
(obj.name = obj.name)(); // window

(obj.getName = obj.getName)赋值语句返回的是等号右边的值,在全局作用域中返回,所以(obj.getName = obj.getName)();this

设计私有的方法和变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和函数内定义的其他函数。

把有权访问私有变量的公有方法称为特权方法(privileged method)。

function Animal() {
    // 定义私有变量series和run
    var series = '哺乳动物';
    function run() {
        console.log('I can run!');
    };
    // 特权方法,可以访问私有变量series
    this.getSeries = function() {
        return series;
    }
}
var dog = new Animal();
console.log(dog.getSeries()); // 哺乳动物

了解两个概念:

  1. 单例:只有一个实例的对象。JS中通常用字面量来创建单例
  2. 模块模式:我的理解就是用闭包的方式为单例创建私有方法和私有属性的模式

用普通模式创建单例:

var objA = {
    name: 'Jane',
    speak: function() {
        console.log('I can speak!');
    },
    getName: function() {
        return this.name;
    }
}

用模块模式创建单例:

var listObj = (function() {
    // 定义私有变量
    var list = [];
    // 定义特权方法
    var add = function(item) {
        list.push(item);
    };
    var getAll = function() {
        return list;
    };
    var getLength = function() {
        return list.length;
    };
    return {
        add: add,
        getAll: getAll,
        getLength: getLength
    };
})()
listObj.add({
    id: 0,
    name: 'hahaha'
});
console.log(listObj.getAll()); // [{ id: 0, name: 'hahaha' }]
console.log(listObj.getLength()); // 1

闭包的缺陷

闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。

如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

一道面试题

function fun(n, o) {
    console.log(o);
    return {
        fun: function(m) {
            return fun(m, n);
        }
    };
}
var a = fun(0); // undefined,同时a={fun:funtion(m){return fun(m, 0)}}
a.fun(1); // 0,同时得到{fun:funtion(1){return fun(1, 1)}} 外层函数传入n=1,内层函数的n也被置为1
a.fun(2); // 0
a.fun(3); // 0

var b = fun(0).fun(1); // 0
b.fun(2); // 1
b.fun(3); // 1

var c = fun(0).fun(1).fun(2).fun(3); // undefined 0 1 2
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值