JavaScript学习-第七天

2025-09-30
JavaScript笔记

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()不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。