函数
JavaScript语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处.
JavaScript中有三种函数类型:函数声明,函数表达式和Function构造函数创建的函数。
- 函数声明(FD)
函数声明: function 函数名称 (参数:可选){ 函数体 }- 有特定的名字
- 在进入上下文阶段创建
- 影响变量对象(在变量对象里)
- 可以在全局环境或函数体内声明(不能在表达式或一个代码块中声明)
- 可以在定义之前调用函数
// 直接在全局上下文中
function foo() {
// 或者在一个函数的函数体内
function innerFD() {}
}
- 函数表达式(FE)
函数表达式:function 函数名称(可选)(参数:可选){ 函数体 }- 名称可选
- 在代码执行阶段创建
- 不影响变量对象(不进入变量对象)
- 总处于表达式的位置
- 在定义之前不可调用,在定义阶段之后也不调用,只能在代码执行阶段可以调用
如果没有函数名,肯定是函数表达式。
如果有函数名,根据上下文来判断:
如果函数在全局环境或函数体内,就是函数声明;如果是表达式的一部分,就是函数表达式。
函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号
function foo(){} // 函数声明,在全局环境中声明
var bar = function foo(){}; // 函数表达式,是赋值表达式的一部分
new function bar(){}; // 函数表达式,因为它是new运算符的一部分
(function(){
function bar(){} // 函数声明,在函数体中声明
})();
//圆括号、逗号、非等操作符可以将函数转换成函数表达式
var foo = function _foo() {}; // 赋值运算符
(function foo() {});// 圆括号(分组运算符),
[function bar() {}];// 在数组初始化器内
1, function baz() {};// 逗号运算符
!function () {}(); // 逻辑运算符
函数表达式在在定义之前不可用,定义阶段之后也不可用,因为他不在变量对象中
console.log(foo); // foo is not defined
(function foo() {});
console.log(foo); // foo is not defined
函数表达式不存在变量对象中,可以结合能影响变量对象的变量(初始化为undefined),对其进行访问。
console.log(foo); // undefined
var foo = (function foo() {});
console.log(foo); // function foo() {}
采用函数表达式声明函数时,在function命令加上函数名称为命名函数表达式,该函数名只在函数体内有效,在函数体外无效。
这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。
(function foo(bar) {
if (bar) {
console.log(1);
return;
}
foo(true); // 1 可以调用
})();
// 在外部,不可以调用
foo(); // foo is not defined
- Function构造函数
可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,其他参数都是函数的参数,如果只有一个参数,该参数就是函数体。Function构造函数可以不使用new命令,返回结果完全一样。
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
- 函数作用域
作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。
在函数外部声明的变量就是全局变量,它可以在函数内部读取。
在函数内部定义的变量,外部无法读取,称为“局部变量”。
对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明(if、for等),一律都是全局变量。
函数本身也是一个值,也有自己的作用域(保存在内部属性[[scope]]中),它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
// 函数在全局环境声明,所以它的作用域绑定全局作用域
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
// x.[[scope]] = globalContext.VO
// 函数x在调用时:x.scope = [AO,x.[[scope]]] = [AO,globalContext.VO]
// 查找变量a,x.AO中没有a,从作用域链中查找,在globalContext.VO找到a=1
// 函数在另一个函数内部声明,它的作用域绑定外层函数的作用域
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
// foo在全局环境创建:foo.[[scope]] = globalContext.VO
// 函数bar在函数foo内部创建:bar.[[scope]] = foo.scope
// foo调用时:foo.scope = [AO,foo.[[scope]]] = [AO,globalContext.VO]
// bar调用时:bar.scope = [AO,bar.[[scope]]] = [AO,foo.AO,globalContext.VO]
// bar内部代码执行,查找变量x,bar.AO中没有x,从作用域链找查找,在foo.AO中找x=1;
// 这种一个函数访问了另一个函数内的变量的机制就是闭包。
- 函数参数
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递,在函数体内修改参数值,不会影响到函数外部。
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(传入函数的原始值的地址),在函数内部修改参数,将会影响到原始值。
如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = {p: 1};
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
// 对o的修改都会反映在obj身上。
// 但是,如果对o赋予一个新的值,就等于切断了o与obj的联系,导致此后的修改都不会影响到obj。
var obj = [1, 2, 3];
function f(o){
o = [4, 5, 6]; // 将传入的引用指向另外的值
}
f(obj);
console.log(obj); // [1, 2, 3]
// 在函数内部,形式参数o与实际参数obj存在一个赋值关系:o = obj
// 此时obj的引用就复制一份传递给o,对o的修改会反映到obj上
// 当对o重新赋值时,o的引用就修改了,不再指向obj,不会影响obj
某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。
var a = 1;
function f(p) {
window[p] = 2;
}
f('a');
a // 2
// 变量a本来是按值传递,但是写成window对象的属性,就达到了传址传递的效果。
如果有同名的参数,则取最后出现的那个值。
- arguments对象
arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
arguments对象除了可以读取参数,还可以为参数赋值(严格模式不允许这种用法)。
虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。
可以通过apply方法,把arguments作为参数传进去,这样就可以让arguments使用数组方法了。
// 用于apply方法
myfunction.apply(obj, arguments).
// 使用与另一个数组合并
Array.prototype.concat.apply([1,2,3], arguments)
将arguments转为真正的数组。
var args = Array.prototype.slice.call(arguments);
arguments对象带有一个callee属性,返回它所对应的原函数。可以通过arguments.callee,达到调用函数自身的目的。
- 立即调用函数表达式(IIFE)
在Javascript中,一对圆括号()是一种运算符,跟在函数名之后,表示调用该函数。
有时,我们需要在定义函数之后,立即调用该函数。这时,不能在函数的定义之后直接加上圆括号,这会产生语法错误。
因为,function这个关键字即可以当作语句,也可以当作表达式。
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。
为了避免解析上的歧义,JavaScript引擎规定,如果function关键字出现在行首,一律解释成语句。
因此,JavaScript引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。
// 语句
function f() {}
// 表达式
var f = function f() {}
// 函数定义后直接加(),会报错
function(){ /* code */ }();// SyntaxError: Unexpected token (
// 在函数定义后直接加(),再中()加如表达式,不会报错,但不会立即执行
function foo() {
console.log(1)
}(1)
// 因为这相当于在函数声明后,又声明了一个毫不相干的表达式,并不是调用函数
解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”IIFE。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
//注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个IIFE,可能就会报错。
任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,new关键字也能达到这个效果。
通常情况下,只对匿名函数使用这种“立即执行函数表达式”。
它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是模仿块级作用域,可以封装一些外部无法读取的私有变量。
- 闭包
由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
// 利用闭包读取函数内部的变量
var foo = {};
(function init() {
var x = 10;
foo.bar = function () { //对外接口foo.bar
console.log(++x);
};
})();
foo.bar(); // 11
foo.bar(); // 12
console.log(x); // x is not defined 外部无法直接访问
// 第一次调用foo.bar()时:
// foo.bar().Scope: [AO, initContext.AO]
// 查找x,在initContext.AO中找到x=10,前缀自增后,x=11,再输出
// 第二次调用foo.bar()时,查找x=11,前缀自增后,x=12,再输出
// 初始化函数表达式的名称(init)可省略。
// 利用闭包保存状态
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
}
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
// 修改后
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function() {
console.log(num);
};
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
//data[0]()执行时:data[0]Context.Scope: [AO, 匿名函数Context.AO,globalContext.VO]
//data[0]Context.AO中没有num值,沿着作用域链从匿名函数Context.AO中查找,找到num=0
使用立即调用函数表达式来模仿块级作用域,创建私有变量和私有方法,使用闭包创建共有变量和方法。
(function() {
// 私有变量
var age = 26;
var name = 'Leo';
// 私有方法
function getAge() {
return 'your age is' + age;
}
// 共有方法
function getName() {
return'your name is' + name;
}
// 将引用保存在外部执行环境的变量中,形成闭包,防止该执行环境被垃圾回收
window.getAge = getAge;
})();
jQuery中模块与闭包
// 使用函数自执行的方式创建模块
(function(window, undefined) {
// 声明jQuery构造函数
var jQuery = function(name) {
// 主动在构造函数中,返回一个jQuery实例
return new jQuery.fn.init(name);
}
// 添加原型方法
jQuery.prototype = jQuery.fn = {
constructor: jQuery,
init:function() { ... },
css: function() { ... }
}
jQuery.fn.init.prototype = jQuery.fn;
// 将jQuery改名为$,并将引用保存在window上,形成闭包,对外开发jQuery构造函数,这样就可以访问所有挂载在jQuery原型上的方法了
window.jQuery = window.$ = jQuery;
})(window);
// 在使用时,我们直接执行了构造函数,因为在jQuery的构造函数中通过一些手段,返回的是jQuery的实例,所以我们就不用再每次用的时候在自己new了
$('#div1');
《JavaScript高级程序设计》
《JavaScript 标准参考教程》
汤姆大叔-深入理解JavaScript系列