主要内容:
- 函数也是对象
- 函数的匿名表达式
- 函数的参数
- 函数名提升
-
eval
与Function
JavaScript
中, 函数是⼀等公民. 实际上所有的东⻄都需要依附于函数. 前⾯已经学习过函数的基本⽤法, 接下来详细讨论⼀下函数的更深层次的内容.
函数也是⼀个对象
在
JavaScript
中函数也是⼀种数据类型.
function foo() {}
console.log(typeof foo); // => function
也就是说, 函数的地位与对象的地位是⼀样的, 与数字
123
, 字符串"abc"
⼀样, 是有类型的⼀种数据. 因此也就有了函数表达式的概念 (后⾯介绍).
Function 函数
在
JavaScript
中, 有⼀个Function
函数, 它是所有函数的类型, 每⼀个函数都是Function
的实例. 定义⼀个函数, 除了使⽤function
关键字定义, 同样可以使⽤Function
来表⽰, 两种表⽰⽅法等价.
定义
Function
的语法:var func = new Function(参数1, 参数2, ..., ⽅法体);
在这个语法结构中, 构造函数Function
的参数⾄少有⼀个, 参数都是字符串类型. 多个参数分别表⽰定义函数时的参数与⽅法体.
例如, 定义⼀个加法的函数:
function func (num1, num2) {
return num1 + num2;
}
这个函数有两个参数, 分别是
num1
, 和num2
. ⽅法体是return num1 +num2;
. 因此将其改写为Function
的形式:
var func = new Function("num1", "num2", "return num1 + num2";);
调⽤函数直接使⽤func
即可:
console.log(func(1, 2)); // => 3
可⻅使⽤⽅法与直接定义函数⼀样.
改写下⾯函数为
Function
形式
// 求最⼤值
function max(num1, num2, num3) {
var max;
if (num1 >= num2) {
if (num1 >= num3) {
max = num1;
} else {
max = num3;
}
} else {
if (num2 >= num3) {
max = num2;
} else {
max = num3;
}
}
return max;
}
⾃定义函数是 Function
的对象
可⻅
new Function(...)
后得到的是⼀个函数. 实际上, ⾃定义的每⼀个函数都是⼀个对象, 是Function
的实例.
在JavaScript
中要判断⼀个对象是否为某个构造⽅法创建出来的, 可以使⽤instanceof
运算符, 其语法为:
实例 instanceof 构造函数
如果实例是被构造⽅法创建出来的, 该表达式返回true
; 如果实例是继承⾃构造函数创建出来的对象的, 表达式返回true
. 否则返回false
function foo1() {}
var o1 = new foo1();
function foo2() {}
function foo3() {};
foo3.prototype = o1; // 设置原型
var o2 = new foo3(); // o2 继承⾃ o1
console.log(o2 instanceof foo1); // => true
console.log(o2 instanceof foo2); // => false
console.log(o2 instanceof foo3); // => true
使⽤
instanceof
运算符, 可以验证⾃定义函数是否为Function
的实例:
function foo1() {}
function foo2() {}
console.log(foo1 instanceof Function); // => true
console.log(foo1 instanceof Array); // => false
console.log(foo1 instanceof foo2); // => false
函数的字⾯量
函数在使⽤的时候也可以动态的赋值, 使⽤的便是函数的字⾯量. 其语法为:
function (参数, ...) { 函数体; }
函数的字⾯量, 和数字字⾯量, 字符串字⾯量, 或对象字⾯量⼀样, 表⽰的就是函数本⾝. 使⽤的使⽤可以使⽤⼀个变量来接收, 再使⽤该变量来调⽤:
var foo = function() {
console.log("函数表达式");
}; // 赋值表达式, 需要使⽤分号结束
foo(); // 调⽤
或者使⽤⾃调⽤函数:
(function () {
console.log("⾃调⽤函数");
})();
注意: 使⽤函数字⾯量的时候, 使⽤的是赋值语句, 因此需要使⽤分号结尾.
Function 的原型
类似于对象和
Object.prototype
的关系⼀样. ⾃定义函数是Function
的实例. 因此⾃定义函数的⾮正式属性__proto__
与Function.prototype
的引⽤相同:
function foo1() {}
var foo2 = function() {};
console.log(foo1.__proto__ === Function.prototype); // => true
console.log(foo2.__proto__ === Function.prototype); // => true
因此
Function对象-图1.pngFunction.prototype
就是任意函数的原型. 奇怪的是Function
也是函数, 因此Function.__proto__
与Function.prototype
引⽤也相等.
console.log(Function.__proto__ === Function.prototype);
也就是说任意函数都继承⾃Function.prototype
对象.
因此如果需要给任意⼀个函数都添加⽅法, 可以使⽤下⾯语法:
Function.prototype.showName = function() {
console.log(/function\s(.+?)\(/.exec(this.toString())[1]);
};
function jkmethod() {}
jkmethod.showName();
给
Function.prototype
添加的任何成员, 都会被继承到任意函数中. 因此在Douglas
的JavaScript: The Good Parts
中有下⾯的经典代码 (后⾯解释):
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
} // 任意⽅法都有 method ⽅法, 给当前⽅法的原型添加新⽅法
函数参数
函数在调⽤的时候, 为了更好的复⽤, 需要给其传递参数. 在本节中主要讨论三个话题: 形参与实参的概念, 没有函数重载, 以及
arguments
对象.
形参与实参
在函数的术语中有两个概念, ⼀个是形参⼀个是实参. 那么怎么理解呢? ⾸先看看形式计算的概念.
所谓的形式计算, 就是符号计算, 不涉及具体的数据. 取⽽代之的是符号的规律与逻辑运算符. 例如中学阶段学习的代数就是典型的形式计算. 例如:
$$ (a + b)^2 = a^2 + 2ab + b^2 \ \int_a^b f(x) \rm{d}x = \left.F(x)\right|_a^b$$
那么将运算可以从具体的数据转到符号逻辑推导中. 将繁复的计算过程简化成⼏个简单的公式, 然后带⼊数据完成计算. 这便是形式计算.
那么形参是什么呢? 定义⼀个函数, 例如打印⼀个数字
function showNum(num) {
console.log(num);
}
函数体就像数学符号表达式, 整个逻辑就是基于参数这个符号进⾏处理的⼀段逻辑. 打印的是参数
num
. 也就是说整个函数体都是围绕参数这个符号来执⾏的逻辑, 不涉及到具体的参与数据 (函数体中定义的常量数据不算), 因此就是⼀个形式逻辑结构. 所以将函数定义中, 定义的参数称为形式参数.
实际参数, 顾名思义就是实际的数据, 即具体参数运算的数据. 将函数定义中的参数称为形参, 将函数调⽤时传⼊的参数称为实际参数.
function max(num1, num2) { // 参与函数逻辑的符号, num1, num2 是形式参数
return num1 > num2 ? num1 : num2;
}
var res1 = max(12, 23); // 传⼊参与运算, 数字 12, 23 是实际参数
var n1 = 34, n2 = 45;
var res2 = max(n1, n2); // 传⼊参与运算, 变量 n1, n2 是实际参数
没有函数重载
在
C++
中有函数重载的概念, 所谓的函数重载, 是指允许定义多个函数, 函数名相同, 但是要求参数不同. 那么在JavaScript
中是不⽀持的.
function foo() {
console.log("⽆参数函数 foo");
}
function foo(n) {
console.log("⼀个参数函数, 参数是 " + n);
}
function foo(n, m) {
console.log("两个参数函数, 参数是 " + n + " 和 " + m);
}
foo();
foo(1);
foo(1,2);
从词法分析的顺序看, 后⾯的函数会覆盖前⾯的函数.
arguments 对象
函数没有重载, 但是很显然, 前⾯介绍的
Function
在调⽤的时候可以有多个参数. 那么是如何实现的呢? 实际上每⼀个函数内部都有⼀个arguments
对象. 它是⼀个像数组⼀样的对象, 描述了参数的信息与函数的信息.
作为对象 arguments
有下⾯属性
-
length
+ 索引 callee
arguments
是⼀个像数组的对象, 因此有length
属性, 表⽰他⾥⾯有多少数据. 同时可以使⽤索引来访问每⼀个数据:
function foo() {
for (var i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
foo(1,2,3);
执⾏结果为
Function对象-图2.png
可⻅,arguments
中存储的就是传⼊的参数. 可以将其看成参数数组 (注意, 是看成数组).
利⽤
arguments
对象, 可以判断传⼊的参数是什么, 以及传⼊的参数个数. 那么根据个数可以做出不同的判断, 完成不同的事情.
将前⾯的foo
函数进⾏改写, 既有
function foo() {
var len = arguments.length;
switch (len) {
case 0:
console.log("⽆参数函数 foo");
break;
case 1:
console.log("⼀个参数函数, 参数是 " + arguments[0]);
break
case 2:
console.log("两个参数函数, 参数是 " + arguments[0] + " 和 " + arguments[1]);
break;
default:
console.log("参数依次是: " + [].join.call(arguments, ", "));
break;
}
}
foo();
foo(1);
foo(1, 2);
foo(1, 2, 3, 4, 5, 6);
例: 求多个数字的和:
function getSum() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
例: 简单模拟 jQuery 的 onload 与 id 选择
function J() {
var len = arguments.length,
arg = arguments[0];
if (len != 1) return;
if (typeof (arg) === "function") {
window.onload = arg;
}
if (typeof (arg) === "string") {
return document.getElementById(arg);
}
}
callee
是当前函数引⽤. 即在函数foo
内部,arguments.callee
就是函数foo
本⾝.
function foo() {
console.log(foo === arguments.callee);
}
foo(); // => true
⼀般来说⽤不着该属性, ⽽且在严格模式中,
callee
是未定义. 但是使⽤它,可以⽀持匿名函数的递归.
函数名提升
函数有定义语法, 也有字⾯量语法. 虽然⼏乎是⼀样的, 但是还是有⼀点点的异同. 先来看⼀个案例:
var num = 123;
function foo() {
console.log(num);
var num = 456;
console.log(num);
}
foo();
原因是, 在
javascript
中,javascript
引擎在解释代码的时候, 会将所有的变量声明提升到最前⾯. 因此在函数foo
⾥⾯, 代码就会变成:
var num = 123;
function foo() {
var num;
console.log(num);
num = 456;
console.log(num);
}
foo();
打印未初始化的变量, 当然是
undefined
. 这样虽然变量名定义在下⾯, 但是感觉好像将变量定义在前⾯⼀样. 这个的结论我们成为"变量名提升"
. 同样我们说过: 函数也是⼀中数据类型, 和其他类型⼀样. 因此存储变量名提升, 也存在函数名提升
.
var foo = function() {
console.log("foo");
}
var func = function() {
foo();
var foo = function() {
console.log("func.foo");
}
}
func();
运⾏结果是抛出异常. 原因很简单, 和变量名提升⼀样, 函数名也会提升, 修改⼀下代码:
var foo = function() {
console.log("foo");
}
var func = function() {
var foo;
foo();
foo = function() {
console.log("func.foo");
}
}
func();
很明显,
foo
是undefined
, ⾃然也就不能当做函数来调⽤. 所以出现错误.
那么如果使⽤函数的声明语法呢? 那就会正常执⾏. 原因是在
javascript
引擎解析代码的时候. ⾸先会将函数的声明语法全部加载⼀遍, 因此在执⾏赋值语句的时候, 内存中早已有函数的声明了. 因此调⽤就不会出错啦.
function foo() {
console.log("foo");
}
function func() {
foo();
function foo() {
console.log("func.foo");
}
}
func();
由于有了这些不同, 因此在实际开发的时候, 推荐将变量都写在开始的地⽅,也就是在函数的开头将变量就定义好, 类似于
C
语⾔的规定⼀样. 这个在javascript
库中也是这么完成的, 如jQuery
等.
问题: 下⾯代码执⾏结果是什么
// 1.
var a = 123;
function a() {
console.log("a function");
}
console.log(a);
// 2.
b();
function b() {
console.log("b function");
}
// 3.
c();
var c = function () {
console.log("c function");
};
eval 函数与 Function 函数
在
javascript
有⼀个函数eval
⾮常强⼤. 它可以动态的执⾏脚本. 将字符串作为参数传⼊, 然后函数执⾏的时候就会将这个字符串解析成脚本执⾏. 例如:
console.log(num); // => 报错
如果加上eval
代码:
eval("var num = 123;");
console.log(num); // => 123;
可⻅
eval
的功能⾮常强⼤. 当然也有⼈会认为这个写法完全是浪费表情. 当然实际开发中不会这么使⽤. 这⾥只是为了说明它的基本⽅法⽽已. 那么在实际开发中可以利⽤它实现⼀些较为灵活的功能. 例如绘制函数曲线. 可以根据⽤户的输⼊决定函数是什么, 然后再进⾏绘制. 再⽐如利⽤ajax
获得数据. 那么实际开发中⼀般得到的是json
格式的字符串, 那么完全可以使⽤该函数将字符串变成对象.
var txt = '{name: "JK", course: "JavaScript ⾯向对象"}';
var o = eval("(" + txt + ")");
当然远远这么些, 凡是动态处理的东⻄都可以利⽤它来实现.
但是
eval
函数也有它的弊端. 因为它会不经意的污染全局变量. 因此⼀般开发中推荐使⽤Function
来代替eval
var txt = '{name: "JK", course: "JavaScript ⾯向对象"}';
var o = (new Function("return " + txt))();
⼩结
- 函数也是对象, 是
Function
的实例 -
Function
既是函数, ⼜是对象 -
Function.prototype
是函数对象的原型 -
arguments
对象 - 函数名提升
- 使⽤
eval
和Function
动态的执⾏脚本
网友评论