【JavaScript】函数:闭包、立即调用的函数表达式、eval命令

1 闭包

JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。正常情况下,函数外部无法读取函数内部声明的变量。

  • 如果需要得到函数内的局部变量,正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
/*
	JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。
*/
function f1() {
  var n = 999;
  function f2() {
  console.log(n); // 999,f1内部的所有局部变量,对f2都是可见的
  }
}
  • 既然f2可以读取f1的局部变量,那么只要把f2作为返回值,就可以在f1外部读取它的内部变量了。
function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。

  • 闭包的最大用处有两个:1. 可以读取外层函数内部的变量,2. 就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
/*
	闭包使得内部变量记住上一次调用时的运算结果。
*/
function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。
闭包用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

  • 闭包的另一个用处,是封装对象的私有属性和私有方法。
function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

2 立即调用的函数表达式(IIFE)

  • 函数定义后立即调用:将函数放在一个圆括号里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表达式,而不是函数定义语句,所以就避免了错误。

  • 任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面的写法。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
  • 通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:1. 不必为函数命名,避免了污染全局变量;2. IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 写法二,写法二比写法一更好,因为完全避免了污染全局变量。
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

3 eval命令

3.1 基本用法

  • eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。
eval('var a = 1;');
a // 1
  • 如果eval的参数不是字符串,那么会原样返回。
eval(123) // 123
  • eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
var a = 1;
eval('a = 2');

a // 2
  • 如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。
(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()
  • 不过,即使在严格模式下,eval依然可以读写当前作用域的变量。
(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

3.2 eval的别名调用

  • 引擎在静态代码分析的阶段,根本无法分辨执行的是eval。
var m = eval;
m('var x = 1');
x // 1

变量m是eval的别名。静态代码分析阶段,引擎分辨不出m(‘var x = 1’)执行的是eval命令。

  • 凡是使用别名执行eval,eval内部一律是全局作用域。
var a = 1;

function f() {
  var a = 2;
  var e = eval;
  e('console.log(a)');
}

f() // 1
  • eval的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨eval()这一种形式是直接调用。
/*
	这些形式都是eval的别名调用,作用域都是全局作用域。
*/
eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')
  • 22
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值