arguments 对象
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。
arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用.
var f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3
正常模式下,arguments对象可以在运行时修改。
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
上面代码中,函数f()调用时传入的参数,在函数内部被修改成3和2。
严格模式下,arguments对象与函数参数不具有联动关系。也就是说,修改arguments对象不会影响到实际的函数参数。
var f = function(a, b) {
'use strict';
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
上面代码中,严格模式下,修改arguments对象不会影响到实际的函数参数a和b。
通过arguments对象的length属性,可以判断函数调用时到底带几个参数。
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
与数组的关系
需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。
如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
callle 属性
arguments对象还有一个callee属性,返回它所对应的原函数。
var f = function(a, b) {
return arguments.callee(a) + b;
}
f(1, 2) // 3
上例中,arguments.callee返回的正是函数f。简单解释一下,arguments.callee就是arguments对象对应的函数f,所以可以用它来调用函数f。
闭包
闭包(closure)是指有权访问另一个函数作用域中的变量的函数。创建闭包的最常见的方式就是在一个函数内部创建另一个函数。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上例中,函数f2就构成了闭包。它可以在自己的作用域以外被调用,调用时可以使用自己作用域内的变量。
闭包的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc)用到了外层变量(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的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
立即调用的函数表达式(IIFE)
立即调用的函数表达式(Immediately Invoked Function Expression),简称IIFE。它的最大用处有两个,一个是不必为函数命名,避免了污染全局变量,二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
(function () {
var a = 'Hello';
console.log(a);
})();
// Hello
上面代码中,定义了一个匿名函数,然后立即调用它。这样做的目的是,不将不必要的全局变量添加到全局环境,防止变量名冲突。
如果要将一个对象的方法,赋值给一个变量,这时也可以使用 IIFE。
var print = (function () {
function print(s) {
console.log(s);
}
return print;
})();
print('hello')
// hello
eval 命令
eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。
eval('console.log(123)');
// 123
eval的参数是一个字符串,所以必须放在引号里面。如果引号省略,就变成了变量,就会报错。
eval(console.log(123));
// ReferenceError: console is not defined
eval的一个重要用途是将字符串转为对象。
var obj = eval('({a:1, b:2})');
obj // {a: 1, b: 2}
eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
var a = 1;
eval('a = 2');
a // 2
上面代码中,eval命令修改了外部变量a的值。由于这个原因,eval有安全风险。
为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()
上面代码中,函数f内部是严格模式,这时eval内部声明的foo变量,就不会影响到外部。
不过,即使在严格模式下,eval依然可以读写当前作用域的变量。
(function f() {
'use strict';
var foo = 1;
eval('foo = 2');
console.log(foo); // 2
})()
上面代码中,严格模式下,eval内部还是改写了外部变量,可见安全风险依然存在。
总之,eval的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,一般不推荐使用。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法。
eval 的别名调用
就是把eval赋值给一个变量,然后通过这个变量调用eval。
var geval = eval;
geval('2 + 2'); // 4
var m = eval;
m('var x = 1');
x // 1
上面代码中,变量m是eval的别名。静态代码分析阶段,引擎分辨不出m('var x = 1')执行的是eval命令。
为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一律是全局作用域。
var a = 1;
function f() {
var a = 2;
var e = eval;
e('console.log(a)');
}
f() // 1
上面代码中,eval是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的a为全局变量。这样的话,引擎就能确认e()不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。