本文为【JavaScript 漫游】专栏的第 005 篇文章,记录了 JS 数据类型 function 的重要知识点。
new Function()
name
属性、length
属性和 toString
方法arguments
对象IIFE
eval
命令函数的三种声明方式
JS 有三种声明函数的方法:使用 function
命令的声明式方法、采用变量赋值的函数式方法,还有使用 new Function()
的鸡肋方法。
function print(s) {
console.log(s);
}
var print = function(s) {
console.log(s);
}
var add = new Function(
'x',
'y',
'return x + y'
); // 最后一个参数是函数体,前面的参数都是函数的参数
new Function() 相当鸡肋,几乎不用
函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
函数是第一等公民
JS 把函数看作一种值,函数是一个可以执行的值,此外并无特殊之处。
由于函数与其他数据类型地位平等,所以 JS 中又称函数为第一等公民。
function add(x, y) {
return x + y;
}
// 将函数赋值给一个变量
var operator = add;
// 将函数作为参数和返回值
function a(op){
return op;
}
a(add)(1, 1)
// 2
函数名的提升
JS 引擎把函数名视同变量名,所以采用 function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。
// 我们写的代码
f();
function f(){}
// 实际运行的代码
function f(){}
f();
对于函数名的提升,要注意的是,当 function
命令和 var
命令同时声明一个函数时,后者会覆盖前者。
// 我们写的代码
var f = function (){
console.log('2');
};
function f() {
console.log('1');
}
f();
// 实际运行的代码
function f() {
console.log('1');
}
var f;
f = function (){
console.log('2');
};
f();
name
属性、length
属性和 toString
方法
name
属性返回函数名length
属性返回函数定义的参数个数toString
方法返回函数的源码function add (x, y) {
return x + y;
}
add.name; // 'add'
add.length; // 2
add.toString();
// function add (x, y) {
// return x + y;
// }
Math.sqrt(); // function(){[native code]}
要注意的是,对于 JS 原生的函数,toString
方法返回 function(){[native code]}
。
函数作用域
作用域指的是变量存在的范围。在 ES5 规范中,只有全局作用域和函数作用域。全局作用域中的变量,在所有地方都可以读取。函数作用域中的变量,只能在函数内部读取。
要注意的是,函数作用域中的变量如果和全局作用域中的变量同名,那么在函数内部读取该变量时,读取的是函数作用域中的变量。
var x = 1;
function f () {
var x = 2;
console.log(x);
}
f(); // 2
x; // 1
此外,在前面的文章中,笔者记录了变量提升这一概念,JS 引擎先解析代码,获取所有的变量,然后在运行代码,这意味着全局作用域下声明的变量在运行时都会被提升到代码头部,而在函数作用域下声明的变量在运行时则会提升到函数体的头部。
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
最后,函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时的作用域无关。
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
简言之,关于函数作用域,笔者认为要记住以下三点:
var
命令声明的变量存在变量提升问题,全局作用域下会提升到全局代码头部,函数作用域下会提升到内部函数体代码头部。JS 参数省略
函数参数不是必需的,JS 允许省略参数。
function f(a, b) {
return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2
同名参数
如果有同名的参数,取最后出现的那个值。
arguments 对象
由于 JS 允许函数有不定数目的参数,所有需要一种机制,可以在函数体内部读取所有参数。这就是 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
注意,如果开启了严格模式,arguments
对象与函数参数就不再具有联动关系,修改 arguments
对象不会影响到实际的函数参数。
var f = function(a, b) {
'use strict'; // 开启严格模式
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
通过 arguments
对象的 length
属性,可以判断函数调用时到底带几个参数。
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
闭包
ES5 规范中,JS 有两种作用域:全局作用域和函数作用域。函数内部可以读取全局变量,但是函数外却无法读取函数内部的局部变量。
出于种种原因,需要得到函数内的局部变量,所有就需要用到闭包。
闭包,就是函数内部,再定义一个函数,并且定义的函数读取了外部函数中的局部变量。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
闭包的另一个用处,是封装对象的私有属性和私有方法。
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
要注意的是,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
IIFE
IIFE,Immediately-Invoked Function Expression,立即调用的函数表达式。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
作用是定义的函数,可以立即执行。
eval
命令
eval
命令接受一个字符串作为参数,并将这个字符串当作语句执行。
eval('var a = 1;');
a // 1