如果Javascript函数是我心中的那个女孩,那么,就让我们谈一场恋爱吧。
1 初见
初见Javascript函数,其实和其它语言的函数也没什么两样。
1.1 函数的声明
function is_true() { return true; }
用关键字function声明函数,有函数名,函数参数,函数体,一个标准的return关键字返回函数结果。
就连函数的调用也没什么不同:
console.log("call is_true", is_true());
输出结果:call is_true true
函数是一种类型:
console.log("is_true is", typeof is_true);
输出结果:is_true is function
函数是一个对象,在javascript的世界里,除了那几个调皮捣蛋,永远也长不大的基本数据类型以外,所有的东西都是对象。
让我们看看函数对象的名称属性:
console.log("is_true name is", is_true.name);
输出结果:is_true name is is_true
1.2 函数的参数
函数的参数非常简单:
function fun_param_0() { return 0; } console.log("fun_param_0 is", fun_param_0()); function fun_param_1(a) { return a; } console.log("fun_param_1 is", fun_param_1(1)); function fun_param_2(a, b) { return a + b; } console.log("fun_param_2 is", fun_param_2(1, 1));
输出结果:
fun_param_0 is 0
fun_param_1 is 1
fun_param_2 is 2
Javascript函数也支持可变参数:
unction fun_param_v() { var ret = 0; for (var i = 0; i < arguments.length; i++) { ret += arguments[i]; } return ret; } console.log("fun_param_v is", fun_param_v(1, 2, 3, 4));
输出结果:fun_param_v is 10
arguments是函数对象自带的属性,函数刚声明的时候是空的,一旦函数被调用,这个属性就会被实参填满,如你所见,其实arguments就是一个数组。
其实函数声明时对于函数参数完全不用写:
function fun_param_2_not_param() { return arguments[0] + arguments[1]; } console.log("fun_param_2_not_param is", fun_param_2_not_param(1, 1));
输出结果:fun_param_2_not_param is 2
这个函数和上面的fun_param_2函数是完全一致的,当然了,fun_param_2_not_param这种写法并不推荐使用,因为可读性不好。但是,你一定要记住,Javascript作为一门蹩脚的动态语言,函数的参数声明和实参个数不一致是非常容易的,这种口是心非可以看做是她的公主特权,我们必须限制她随意使用特权。
1.3 函数的返回值
函数的返回值非常简单:
function fun_return_0() { var a = 0; } console.log("fun_return_0 is", fun_return_0()); function fun_return_1() { return 1; } console.log("fun_return_1 is", fun_return_1()); function fun_return_v() { return { a : 1, b : 2 }; } var r = fun_return_v(); console.log("fun_return_v is", fun_return_v(), r.a, r.b);
输出结果:
fun_return_0 is undefined
fun_return_1 is 1
fun_return_v is { a: 1, b: 2 } 1 2
如你所见,函数可以返回多个返回值,一般是通过对象的方式。
2 相识
有位美女做朋友是不是很有面子的一件事?关键是,你能不能一个电话就把美女约出来?
2.1 男女平等
在Javascript的世界里,如果函数是女士,对象是男士,那么两者的社会地位是平等的。
本质上,函数就是对象,这是理解Javascript函数的第一准则。
因为函数是对象,所以它可以赋值给一个变量:
var is_false = function() { return false; } console.log("is_false is", typeof is_false); console.log("is_false is", is_false.name);
输出结果:
is_false is function
is_false is is_false
习惯上,这被称为函数表达式。
因为函数是对象,所以它可以作为函数参数:
function is_true() { return true; } function is_false() { return false; } function is_boolean(fn) { console.log(fn()); } is_boolean(is_true); is_boolean(is_false);
输出结果:
true
false
因为函数是对象,所以它可以作为函数返回值:
function is_true() { return true; } function is_false() { return false; } function find_boolean(key) { if (key) { return is_true; } else { return is_false; } } console.log(find_boolean(true)()); console.log(find_boolean(false)());
输出结果:
true
false
所以,在Javascript的世界里,函数是一等公民。这其实是Javascript从出生就自带的基因,因为它的设计人就是搞函数式编程的,所以从那里借鉴了一点东西,没错,就是一点,也可以说Javascript是阉割版的函数式编程语言。
2.2 女士优先
在Javascript的世界里,如果函数是女士,则她拥有无可争辩的优先权,这就是女士优先的原则。
请看下面的代码:
console.log("is_lady type is", typeof is_lady); function is_lady() { } var is_lady = 4;
输出结果:is_lady type is function
这段很简单的代码从其它编程语言的视角来看问题多多:
第一行,is_lady还未被声明,能不能用?
is_lady先被声明为函数,又被声明为变量,哪个优先?
这段代码在Javascript的世界里完全合法,这是由这门语言的本性决定的。
首先,Javascript是一门编译语言,它的正确运行要分两步:先编译,后执行。在编译阶段,编译器会检查所有的函数声明和变量声明,而根本不关心函数是怎么定义的,变量是怎么赋值的。在编译器的眼中,这段代码实际上是下面的样子:
function is_lady() { } var is_lady; console.log(typeof is_lady); is_lady = 4;
如你所见,函数声明和变量声明都被提升了。
其次,Javascript是一门动态语言,它允许一个名称先赋予函数再赋予变量或者反过来。那么在变量声明和函数声明之间哪个优先级更高呢?Javascript规定:函数声明的优先级更高。
特别的,函数还可以多次声明,从而导致了一个覆盖的问题:
is_lady(); function is_lady() { console.log("cover", 1); } function is_lady() { console.log("cover", 2); } var is_lady = function() { console.log("cover", 3); }
输出结果:cover 3
仔细分析一下上面的代码:第二个is_lady覆盖了第一个is_lady,而第三个is_lady又因为是变量,没有前两个is_lady优先级高。
你看到了吧,在Javascript代码中一个作用域内用同样的名字多么恐怖的一件事,这也变相的鼓励我们在代码中最好用不同的名字。
函数优先的原则不仅表现在编译器提升上,更重要的表现其实是函数的嵌套:
function outer() { console.log(1); function inner() { console.log(2); } inner(); console.log(3); } outer();
输出结果:
1
2
3
函数里面还可以有函数,这在其它语言中是很难想象的,但是在Javascript的世界里,函数基本上是想去哪就去哪。其实这也没啥好奇怪的,别忘了我们的第一准则,然后看看下面的代码:
var outer = { a : 1, inner : { b : 2, }, c : 3, }; console.log(outer);
输出结果:{ a: 1, inner: { b: 2 }, c: 3 }
函数中的函数就没啥不好理解的了。
2.3 立即执行
在Javascript的世界里,如果函数是女士,你就得理解她间歇式冲动爆发的行为。
函数声明和函数调用的分离有时候并不是我们期望的,Javascript还支持函数声明和函数调用绑在一起的功能,这通常被称为立即执行函数表达式。
(function buy() { console.log("买,买,买。。。"); }()); (function buy() { console.log("买,买,买。。。"); })(); void function buy() { console.log("买,买,买。。。"); }(); !function buy() { console.log("买,买,买。。。"); }();
输出结果:
买,买,买。。。
买,买,买。。。
买,买,买。。。
买,买,买。。。
看着这一堆的买,买,买,是不是特别有一种剁手的冲动:)
因为都是立即执行函数表达式,所以函数名本身其实并不重要,但是根据Javascript大佬们的建议,最好所有的函数都给个名称。
从语法上来说,下面的代码是编译不过去的:
function buy() { console.log("买,买,买。。。"); }();
因为编译器很难区别函数声明中的()和函数调用中的()。所以通常的做法就是用操作符来改变一下表达式的优先级。
第一种,用()把所有东西都括起来是可以的
第二种,用()把函数声明括起来也是可以的(用()把后面的()括起来就不要想了吧)
第三种,用void运算符
第四种,用!运算符
其实用“+”,“-”,“~”之类的无副作用的单目运算符也是可以的,就是看起来怪怪的,还是推荐用()的方式。
2.4 谁的电话
在Javascript的世界里,如果函数是女士,谁来约她是很关键的。比如谈到她的酒量,如果你是一位帅哥,那个她一杯就倒,如果你是一个loser,那么她千杯不醉。
在其它面向对象的语言中,习惯于将类的成员函数第一个参数定义为this引用(或指针),以标记是哪个对象调用了该函数。Javascript也借鉴了这一点。我们可以在函数中使用this。
function is_this() { return this; } console.log("this is", is_this());
输出结果(浏览器中节选):
输出结果(node.js中节选):
这里的this返回的实际上是全局对象。
函数的中的this引用,网上相关的文档比比皆是,但只要想到我们的第一准则,其实基本规则非常简单:
如果有明显的调用对象,那么this就是这个对象
没有没有明显的调用对象,那么this就是全局对象
基本上,函数调用可以分为四种情况,上面已经演示了第一种,作为函数进行调用,因为没有明显的调用对象,所以this就是全局对象。
第二种,作为方法进行调用:
var i = { call : function() { console.log("一起看电影啊"); return this; } }; console.log("obj call is", typeof i.call(), i.call());
输出结果:
一起看电影啊
一起看电影啊
obj call is object { call: [Function: call] }
这就是有明显的调用对象,所以this就是这个对象。
第三种,作为构造器进行调用:
function Person(name, age) { this.name = name; this.age = age; console.log("new this is", typeof this, this); } var p = new Person("张三", 25); console.log("name:", p.name, "age:", p.age); var q = new Person("李丽", 20); console.log("name:", q.name, "age:", q.age); console.log("name:", p.name, "age:", p.age);
输出结果:
new this is object Person { name: '张三', age: 25 }
name: 张三 age: 25
new this is object Person { name: '李丽', age: 20 }
name: 李丽 age: 20
name: 张三 age: 25
只要函数和new搞在了一起,那就叫构造器。new的特点就是每次都生成新的对象,所以这里的p,q实际上引用的是不同的对象。
这里有明显的调用对象,所以this就是这个对象。
第四种,作为指定进行调用:
function is_this() { return this; } console.log(is_this.call({})); console.log(is_this.call(window)); console.log(is_this.call(null)); console.log(is_this.call(undefined));
输出结果:
call函数允许用户自定义this,你指定那个对象,this就是那个对象,如果找不到,那就是全局对象。
apply函数与此类似,只不过函数参数必须用数组而已。
未完待续。