9.核心JavaScript笔记:函数(function)

本系列内容由ZouStrong整理收录

整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》

关于函数

  • 函数是对一段JavaScript代码的封装,多用于重复完成指定的功能,它只定义一次,但可以被执行或调用任意次
  • 函数是对象(是Function类型的实例),同样具有属性和方法
  • 函数对象,函数名仅仅是一个指向函数对象的指针,不会与某个函数绑定,因此函数可以赋值给其它变量,可以传递给其它函数
  • 函数是参数化的,函数的参数在函数内部就像局部变量一样工作
  • 函数都有返回值,即使未显式指定,也会返回undefined

一. 函数定义

函数使用function关键字定义,后跟可选的函数名(省略函数名将创建匿名函数)、一对圆括号(其中包括了参数列表)、一对花括号

创建函数可以使用三种方式

1. 函数声明

function funcName(a, b,...) { 
	//函数声明,此函数相当于作用域内的一个局部变量
}

2. 函数定义表达式

var funcName = function(a, b,……) {
	//函数定义表达式,创建的是匿名函数,然后赋值被一个变量,局不局部,看var
};

3. 构造函数方式

函数还可以像普通对象一样,使用new来创建

var funcName = new Function("a","b","return a+b;");
//Function构造函数,接收任意数量的字符串参数
//前面的参数作为新函数的参数
  //最后一个参数作为新函数的函数体,可以包含任意JavaScript语句

Function()构造函数创建的也是匿名函数,然后可以赋值给一个变量

4. 再谈函数声明和函数定义表达式

函数声明

  • 函数声明即相当于定义了一个当前作用域中的局部变量
  • 函数有一个非标准的name属性,对于函数声明,name等于函数名

函数定义表达式

  • 函数定义表达式定义的函数是不是局部的,就看有没有使用var了
  • 对于函数定义表达式,非标准的name属性返回空字符串,这也印证了为什么函数定义表达式创建的函数叫做匿名函数(也叫拉姆达函数),它是没有名字的,只不过赋值给了一个变量而已

函数声明提升

JavaScript函数声明的解析是在预执行阶段(pre-execution阶段),即浏览器准备执行代码之前,此时会进行函数声明的提升! 所以通过函数声明定义的函数,可以在函数定义之前被调用

而函数定义表达式的解析是在语句执行到所在行时,所以函数定义表达式只能在其后被调用

fn();   //输出2
var fn= function(){
	console.log(1);
}
function fn(){
	console.log(2);
}
fn();   //输出1

预执行期会处理所有使用var声明的变量和所有函数声明(即变量声明提升和函数声明提升:提升到当前作用域的顶端),不同的是,对于变量来说,只处理声明,不处理赋值,赋值仍在执行期处理;而对函数声明来说,处理声明同时处理函数体

var name = 1;      
function alertName() {      
	if (!name) {      
		var name = 10; 
	}      
	alert(name);      
}      
alertName();      //输出10

再看这个例子

var a = 1;      
function b() {      
	a = 10;         
	function a() {} 
}      
b(); 
alert(a);    // 输出1

此外,还要知道的是,变量声明提升更靠前,也就是说变量声明提升总是提升到函数前面

function x(){ }
var x;
typeof x;     // "function"

立即运行的函数表达式

我们可以使用函数表达式创建一个函数并且立即执行它

(function(){
})();
  • 在涉及到局部变量时常用这种技巧,可以创建一个私有变量
  • 有时看到也会传入window参数,是为了性能考虑(作用域链)

为什么函数要被包在括号里?这是因为JavaScript只能就地运行函数表达式

函数声明不能像上面一样执行

function a(){}();   //error

即使我们把名字去掉,JavaScript还是会发现function这个关键字,把它解析成函数声明

function(){}();   //error

所以,只有把函数包在括号里面,解析器才会认为它是语句的一部分,把它解析为函数表达式

如果函数明显就是一个表达式,那么也不用包起来

var a=function(){}();     //立即创建并执行

(function a(){alert(1)})
a();  //error  a is not defined

一加括号就成为了一个表达式,表达式的值应该赋值给一个变量保存起来,否则就不存在了

var b = (function a(){alert(1)})
b();  //1

立即运行的函数表达式,会有一个问题:如果下一行的第一个字符是下面这五个字符之一: ( 、 [、 / 、+ 、 - ,JavaScript将不对上一行句尾添加分号

var a=5
(function(){alert(a)})();   //error

小结

  • 任何合法的标识符都可以作为函数名,但一定要有语义(最好是动词,getTime(),setDate().....)
  • 函数声明会被提前到当前作用域的顶端,所以可以在定义之前被调用;但是函数定义不会提前(仅仅是变量声明提前了,赋值并没有提前),所以必须先定义,再调用
  • 函数声明并非真正的语句,ECMAScript规范只是将它们作为顶级语句,允许它们出现在全局代码,或者嵌套在其它函数中,但是它们不能出现在循环,条件判断或者try/catch语句,以及with语句中
  • 函数定义表达式可以出现在代码的任何地方

二. 函数调用(this值)

函数内部代码在定义时并不会执行(这个时候,即使有错误,也不会报错),只有调用该函数时,它们才会执行

函数可以通过其函数名来调用,后面跟上一对圆括号和实际参数(如果有的话)

注:函数名后加上圆括号,函数会立即执行,而不加圆括号,只有在获得函数的引用时才会执行(例如事件处理程序中)

由于函数是对象,函数名仅仅是指向函数的指针,不会与某个函数绑定

function sum(num1, num2){ 
	return num1 + num2; 
} 
var anotherSum = sum;        //将函数指针赋值给其它变量,二者指向同一个函数
sum = null;                    //切断了指向函数的指针
alert(anotherSum(10,10));    //不受影响,仍然返回20

有四种方式调用函数

  • 作为普通函数
  • 作为对象方法
  • 作为构造函数
  • 通过call()和apply()方法间接调用

在函数内部,有两个特殊的对象 this 和 arguments(后续),this对象代表函数运行时的上下文环境,this 是动态绑定(运行期绑定)的,也就是说在调用函数之前,this 的值并不确定,因此this 可能会在代码执行过程中引用不同的对象,这完全取决于函数的调用方式,但总有一个原则:this指的是调用函数的那个对象

1. 作为函数调用

函数调用很简单,只要函数名+圆括号+参数即可

setName('strong');

在非严格模式下,this代表window对象
而严格模式下,this则是undefined

var x = 1;
function test(){
	alert(this.x);
	this.x= 0;
}
test();      // 1
alert(x);   //0

作为普通函数调用时,函数通常不会使用this关键字,但是可以用来判断是否是严格模式

var strict = (function(){return !this;}());

2. 作为对象方法调用

方法就是作为对象属性的函数

obj.test=function(){
};
obj.test();

作为方法调用时,可以像普通的属性访问一样,使用方括号语法

obj['test']();  //看起来很别扭,但是完全靠谱

对于参数和返回值的处理,与普通函数调用并没有什么区别,区别在于调用上下文,作为对象方法调用时,函数的调用上下文就是该对象

var obj = {
        x:1,
        y:2,
        add:function(){
            this.z = this.x + this.y;
	}
    }
   obj.add();
   obj.z;    //3

this是一个关键字,不允许被赋值,并且和变量不同,this没有作用域的限制,嵌套的函数不会从它的外部函数中继承this(每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止(永远能找到,所以不再向上追溯),因此永远不可能直接访问外部函数中的这两个变量)

嵌套函数

如果嵌套的函数作为方法调用,其this值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象window就是undefined(严格模式)(符合一般规则)

不管怎样,this都不会指向外层函数的上下文,这属于 JavaScript 的设计缺陷,要想访问外部函数的this值,需要将this值保存在变量中,通常被命名为 that或者self

var obj = {
	fun:function(){
		var self = this
		console.log(this===obj);    //true
		function inner(){
			console.log(this===obj);    //false
			console.log(self===obj);    //true	
		}
                inner();
	}
}
   obj.fun();	

3. 作为构造函数调用

函数调用或者方法调用之前有关键字new,就是作为构造函数调用

var o = new Object();
var o = new Object;  //不需要参数时,圆括号可以省略

构造函数调用会创建并初始化一个新的对象,这个对象继承自构造函数的prototype属性

构造函数负责初始化这个新创建的对象,并将这个对象作为其调用上下文,因此构造函数中可以使用this关键字来引用这个新创建的函数

var obj = new  o.m();   //m中的this并不指代对象o,而是obj

构造函数中通常不使用return关键字,因为会自动初始化对象,当构造函数的函数体执行完成后,它会显式返回

如果显式使用return语句返回一个对象,那么新对象就是这个返回的对象;如果显式使用了return语句,但是没有指定返回值或者返回值是基本数据类型,那么返回的值将被忽略(相当于没有return),初始化正常进行

4. 通过call()和apply()方法调用

函数也是对象,也有方法,其中call()和apply()方法(后续)可以用来间接的调用函数

这两个方法都是显式指定调用所需的this值,也就是说,任何函数都可以作为任何对象的方法来调用,即使函数不是那个对象的方法

这两个方法的第一个参数都是要绑定的对象,后续参数call()方法使用函数原有的参数列表作为参数,apply()使用数组的形式传递原有参数

fun.call(obj,x1,x2);
fun.apply(ob,[x1,x2]);
Math.max.apply(Math,[1,2,3,4]);  //求数值数组最大值

call()和apply()的参数为空、null和undefined时,默认调用全局对象(window)

练习

var x=1;
obj={
	x:2,
	m:function(){
		alert(this.x);
	}
};
obj.m();      //2
(obj.m)();      //2
(obj.m = obj.m)();     //1,严格模式下是undefined(因为这个赋值表达式的值是函数本身,所以 this 的值不能得到维持)
window.onclick=obj.m;    // 1
setTimeout(obj.m , 10);    // 1,,严格模式下是undefined

obj.m(不是obj.m())仅仅是获得了函数的引用(将变量名赋值给对象属性),并没有执行,this实在函数执行时才绑定的

代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果就返回了1

第一次是作为window的方法调用的(这通常不是我们所希望的,解决方法见bind())

超时调用的代码都是在全局作用域中执行的,因此this的值在非严格模式下指向window对象,在严格模式下是undefined

var a=1;
function test(){
	alert(a);
	var a=10;  //变量声明提升,var a 提前,覆盖全局的a
	alert(this.a);
	this.a=100;
}
test();          //输出undefined 和 1
new test();  //输出undefined和undefined

将上面的 var a = 10 改成 a = 10

test();          //输出1 和 10 ,此时全局的a成为100
new test();  //输出100和undefined

三. 函数返回值

JavaScript函数在定义时不必指定是否有返回值,不指定返回值的函数,默认都会返回undefined

可以通过 return语句来显示指定返回值

function test(){
	return true;
}

函数会在执行return语句之后立即退出,继续执行函数外部代码;因此,位于return语句之后的函数内部代码不会执行

return语句也可以不带有任何返回值,此时仅仅表示停止执行函数,并返回undefined

function test(){
	return;
}

四. 函数参数

函数在定义时,可以指定任意数量的命名参数(形式参数),并且不需要指定数据类型,这些参数在函数体内像局部变量一样工作

函数在调用时,不会检查传递进来的实参个数和数据类型

  • 多传的实参自动被忽略(但会保存在arguments对象中)
  • 少传的实参将自动获得undefined值(相当于定义了变量但又没有初始化)
  • 所有参数都是按值传递的,不可能通过引用传递参数

因此,对于形参是否可选,应当保持很好的适应性,因为,你不确定在调用时,到底是不是传递了需要的参数

//将对象的可枚举属性名追加到数组中,并返回数组
function getProperty(obj , /*可选*/arr){
	if(arr===undefined){
		arr=[];      //第二个参数如果省略,则创建一个数组
	}
	for(var pro in obj){
		arr.push(pro);
	}
	return arr;
}
var a1 =getProperty(o1,arr); 
var a2 =getProperty(o2);    //传入一个参数也没有问题

上面的if语句可以替换成

var arr = arr || [];

定义可选形参时,应当在函数定义中使用注释/**/表明形参是可选的,并且可选的参数必须放在最后,如果真的可以省略第一个参数,则必须显示传入null或者undefined来占位

当一个函数的参数列表超过3个时,要记住每个参数的正确顺序有点让人头疼,因此可以通过名/值的形式来传入参数(传入一个对象),这样参数的顺序就无关紧要了

1. 实参对象—arguments

之所以函数在调用时不介意传递进来的参数个数和数据类型,是因为JavaScript中的参数在函数内部是用一个类数组对象来表示的,函数接收到的始终都是这个类数组对象,而不关心对象中包含哪些数值,这个对象就是arguments,它包含着实际传入函数中的所有参数

当调用函数时传入的实参个数超过形参个数时,多传的值可通过实参对象arguments获得(类数组对象)

假设定义函数时,它的形参只有一个x,如果调用时传入两个实参,那么第一个实参可以通过x或者arguments[0]来得到,第二个实参只能通过arguments[1]来得到

1)length属性

arguments对象有一个length属性,返回的是传递进来的实参个数(而非函数定义时的形参)

arguments对象的好处就是可以让函数处理任意数量的实参

function max(/* ... */) {
    var max = Number.NEGATIVE_INFINITY;
    for(var i = 0; i < arguments.length; i++)
    if (arguments[i] > max){
        max = arguments[i];
    }
    return max;
}
//任意数量的实参,都可以处理
var largest = max(10, 100, 1000, 4,10000, 6); //10000

在非严格模式下,x和arguments[0]表示同一个值,修改任何一个都会影响到另一个

function a(x){
	console.log(x);
	arguments[0]=1;
	console.log(x);
}
a(2);     //输出2和1

在严格模式下,arguments对象是只读的,并且是一个保留字,不能作为形参名或者局部变量名

function a(x){
	"use strict";
	console.log(x);
	arguments[0]=1;
	console.log(x)
}
a(2);     //输出2和2

2)callee属性

arguments对象还有callee属性,指代拥有这个arguments对象的函数

function test(num){ 
	if (num <=1) { 
		return 1; 
	} else { 
		return num * test(num-1);
	} 
} 

为了消除上面递归算法与函数名紧密耦合的现象,可以使用arguments.callee

function test(num){ 
	if (num <=1) { 
		return 1; 
	} else { 
		return num * arguments.callee(num-1) 
	} 
}

这样,无论原函数被赋予什么值,都可以保证正常完成递归调用

var trueTest = test; 
test = null;
alert(trueTest(3));  //6

在严格模式下,不能够访问和操作arguments对象的这个属性,否则会报错,此时,可以这样使用(严格模式和非严格模式下都行的通)

var test2 = (function test(num){ 
	if (num <=1) { 
		return 1; 
	} else { 
		return num * test(num-1);
	} 
}); 

2. 实参类型

由于函数定义时的形参并没有指定数据类型,传入实参时也不会做任何类型检测

因此可以采用语义化的命名使得参数类型更加直观

function test(array,number){

}

也可以给参数添加注释,使代码自文档化

function test(array/*数组*/,number/*数值*/){

}

对于可选的参数,可以在注释中标明“这个参数是可选的”

function test(array,number,index/*可选*/){

}

对于不定数量实参,可以使用省略号

function test(/*number.....*/){

}

除此之外,还应该添加必要的类型检查逻辑——因为宁愿程序在传入非法值时报错,也不愿非法值导致程序在执行时报错

3. 没有重载

由于函数参数的特性, JavaScript函数不能像传统意义上那样实现重载。而在其他语言(如Java)中,只要这两个函数接受的参数的类型或数量不同就被视为两个函数

如果在JavaScript中声明了两个名字相同的函数,不管参数如何,后者覆盖前者,因为函数名也就是相当于使用var声明了一个变量(局部变量),只不过保存的是函数指针而已

function add (num1,num2) { 
  alert(num1+num2+1);
} 
function add (num){ 
  alert(num)
} 
add(1,2)    //返回1

涉及到函数定义表达式时,情况可能有点复杂

var a=function(){
    alert(1);
}
function a(){
    alert(2);
}
a();  //1,这是因为变量和函数声明都会提升,但是变量的赋值在原位置进行

通过检查传入函数中参数的类型和数量并作出不同的反应,可以模仿重载

function doAdd(num1, num2) { 
	if(arguments.length == 1) {  
	} else if (arguments.length == 2) { 
	} 
}

五. 函数检测

之前列举了如何检测一个对象是不是数组,对于函数也有一些方法来检测

1. typeof运算符

function strong(){}
typeof strong;  //"function"

2. instanceof运算符

对于同一个网页或者一个全局作用域而言,使用instanceof 运算符即可

strong instanceof Function  //返回true

3. constructor属性(构造函数检测)

strong.constructor === Function;     //true

4. 类特性判断

Object.prototype.toString.call(strong).slice(8,-1);   //"Function"

六. 作为值的函数

在JavaScript中,函数不仅是一种语法,也是值,因此可以将函数赋值给变量,存储在对象属性或数组元素中,甚至将函数作为参数传入另一个函数

函数像任何普通的值一样,可以赋值给其它变量

function strong(){
}
var hello = strong;   //两者指代同一个函数

除了赋值给变量,还可以将函数赋值给对象属性(成为方法)

var obj = {
	test:function(){	
	}
}
obj.test(); 

还可以赋值给数组元素,使用的是匿名函数

var arr = [function(num){return num;},5,6,7];
arr[0](arr[1]);    //5

函数可以作为参数被传递给另一个函数,逻辑上,函数是一个行为,所以传递一个函数就是传递了一个可以被其它程序启动的行为

function a(f){
	f();
}
function b(){
}
a(b);

自定义函数属性

函数是对象,因此也可以拥有属性

当函数需要一个“静态”变量在调用时保持某个值不变,最方便的方式就是定义属性,而不是全局变量,因为后者会让命名空间变得混乱

//初始化函数对象的计数器属性
//因为函数声明提升,所以可以给它添加属性
test.count = 0;
//使用一个属性来记住下一次将要返回的值
function test(){
	return test.count++;
}

下面的例子使用了自身的属性来缓存上一次的计算结果

function factorial(n) {
    if (isFinite(n) && n>0 && n==Math.round(n)) {
        if (!(n in factorial)){
            factorial[n] = n * factorial(n-1);
        }
           return factorial[n];
    }else{
        return NaN;
    }
}
factorial[1] = 1; //初始化缓存
factorial(1);    //1
factorial(2);   //2
factorial(3);  //6 

七. 作为命名空间的函数(模仿块级作用域)

在函数内部声明的变量在整个函数体内都是可见的(包括嵌套的函数),在函数外部是不可见的

不在任何函数内声明的变量都是全局变量,在整个JavaScript中都是可见的

在ECMAScript6之前,是无法声明只在一个代码块内可见的变量的,因此,常常简单的定义一个函数用作临时的命名空间,命名空间内声明的变量都不会污染到全局命名空间

function myModule(){
	//模块代码
	//不会污染全局命名空间
}
myModule();

这样只产生了一个全局变量——myModule(前提:函数内变量都是用var声明),但还不够完美,谁能保证myModule不会重名呢?因此可以直接定义一个匿名函数,并且直接调用(立即调用的函数表达式)

;(function(){
	//模块代码
})();

或者

;(function(){

}())

function之前的左圆括号是必须的,只有这样,解析器才会将其解析为函数表达式;否则,就会被解析为函数声明语句

一般来说,我们都应该尽量少向全局作用域中添加变量和函数,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域

这个例子定义一个扩展函数,将后续参数复制至第一个参数(在多数IE版本中,如果o的属性有一个不可枚举的同名属性,则for/in循环不会枚举o的可枚举同名属性,需予以修复)

var extend = (function() {
	for (var p in {toString: null}) {
		return function extend(o) {
        			for (var i = 1; i < arguments.length; i++) {
            			var source = arguments[i];
            			for (var prop in source){
					o[prop] = source[prop];
				}
        			}
			return o;
		};
	}
	return function patched_extend(o) {
		for (var i = 1; i < arguments.length; i++) {
			var source = arguments[i];
			for (var prop in source){
				o[prop] = source[prop];
			}
			for (var j = 0; j < protoprops.length; j++) {
				prop = protoprops[j];
				 if (source.hasOwnProperty(prop)){
					o[prop] = source[prop];
				}
			}
		}
		return o;
	};
	var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString"];
}());

八. 闭包

JavaScript采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的

也就是,函数定义时的作用域链在函数调用时依然有效

为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的逻辑代码,还必须引用当前的作用域链

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性就是“闭包”

  • 通俗讲:闭包就是有权访问另一个函数作用域中的变量的函数
  • 更通俗的将:就是存在于另一个函数中的函数
  • 匿名函数 !== 闭包,但多数情况下,闭包都是匿名函数

匿名函数仅仅是指没有指定名字的函数

window.onload = function(){};  //匿名函数
setTimeout(function(){},1000);  //匿名函数
var me = function(){};   //匿名函数

注:函数变量可以被隐藏在函数作用域之内,因此看起来是函数将变量包裹了起来,所以叫闭包

从技术角度讲,所有JavaScript函数都是闭包,因为他们都关联到作用域链

定义大多数函数时的作用域链在调用函数时依然有效,当调用函数时闭包所指向的作用域链和定义函数时不是同一个作用域链时,就能看出来了(当一个函数嵌套了一个函数,外部函数将嵌套的函数对象返回时就会发生这种情况,这种模式在JavaScript中很常见

var pro="外部变量";
function test(){
	var pro = "内部变量";
	function f(){
		return pro;
	}
	return f();
}
test();   //返回"内部变量"
........................
var pro="外部变量";
function test(){
	var pro = "内部变量";
	function f(){
		return pro;
	}
	return f;
}
test()();   //依然返回"内部变量"

前面的结果看起来很明显,直接返回内部函数执行的结果,内部函数在内部执行;后面返回的是内部函数,内部函数在外部执行,不应该返回外部变量吗?

前面也说到,函数的执行用到了作用域链,这个作用域链是函数在定义时就创建好的,在作用域链中,外部函数的变量对象始终处于第二位,外部函数的外部函数的变量对象处于第三位,……直至作为作用域链终点的全局执行环境。

在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量

闭包的这个特性异常强大,可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数

小结

一般情况下,函数中定义的变量在该函数执行完毕之后就不再存在了,那么闭包为什么能够访问“不存在的”变量呢?

每次调用JavaScript函数的时候,它都会被推入环境栈中,并会为之创建一个新的对象用来保存局部变量,并把这个对象添加至作用域链中。当函数执行完毕,就从作用域链中将这个绑定变量的对象删除,如果此时不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾收集掉,该函数就会被从栈中弹出

但如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象,如果嵌套函数在外部函数中保存下来,那么它们也会和所指向的变量绑定对象一样当做垃圾回收,但是如果这个函数定义嵌套了函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数,它就不会被当做垃圾收集,并且它指向的变量绑定对象也不会被当做垃圾回收,这就导致了外部函数不会被从环境栈中弹出,即使外部函数已经执行完毕,

注:这就是闭包和垃圾回收之间的关系,使用不慎,很容易造成内存泄露

因此一定要在使用了闭包之后,将返回的引用显式设置为null,以接触对闭包的引用

由于 IE9 之前的版本对BOM对象和DOM对象使用不同的垃圾收集例程(引用计数),因此闭包会导致一些特殊的问题——如果闭包的作用域链中保存着一个HTML元素,那么该元素将无法被销毁

function assignHandler(){
	var element = document.getElementById("s");
	element.onclick = function(){
		alert(element.id);
	};
}

这个闭包创建了一个循环引用(详见第四节),只要匿名函数存在, element所占用的内存就永远不会被回收

function assignHandler(){
	var element = document.getElementById("s");
	var id = element.id;
	element.onclick = function(){
		alert(id);
	};
	element = null;
}

通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存

之前定义了一个函数,用来保存每次返回的值

//初始化函数对象的计数器属性
//因为函数声明提升,所以可以给它添加属性
test.count = 0;
//使用一个属性来记住下一次将要返回的值
function test(){
	return test.count++;
}

但是,这样有一个问题:外部代码可以修改计数器的值,导致该函数不一定产生唯一的整数

但是闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用作私有状态

var test = (function(){
	var count = 0;
	return function(){
		return count++;
	};
})();
test();  //0
test();  //1

我们定义了一个立即调用的函数表达式,该表达式返回一个嵌套的函数,只有该函数能够访问count变量,因此可以保证唯一性

像count一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数都可以访问它,这多个嵌套函数共享一个作用域链

function test(){
	var count = 0;
	return {
		add:function(){return count++;},
		reset:function(){count=0}    //这两个方法都可以访问私有变量count
	}
};
var a = test();
var b = test();
a.add();    //0
b.add();    //0   两个互不干扰,每次调用test()函数都会创建新的作用域链和新的私有变量
a.reset();  //    
a.add();    //0
b.add();    //1	

我们也知道了,在同一个作用域链中定义两个闭包,这两个闭包共享相同的私有变量或变量,这是一种很重要的技术,但要注意那些不希望共享的变量

function test(num){
	return function(){
		return num;
	}
}
var arr=[];
for(var i=0;i<10;i++){
	arr[i]  =test(i);
}
arr[5]();    //很正常的返回5

这段代码利用闭包创建了很多个闭包,但是这种代码很容易产生一个错误,那就是视图将循环代码移入定义这个闭包的函数之内

function test(){
	var arr=[];
	for(var i=0;i<10;i++){
		arr[i]  =function(){return i}
	}
	return arr;
}
var arr = test();
arr[5]();              //返回10

由于这些闭包会共享变量i,当test()返回时,i值是10,所有的闭包都共享着一个值,因此数组中的函数都会返回10

注:关联到闭包的作用域链都是“活动的”,闭包不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态模块

注:闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量

但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期

function test(){
	var arr=[];
	for(var i=0;i<10;i++){
		arr[i] = (function(num){ 
		    return function(){ 
		        return num; 
		    }; 
		})(i);
	}
	return arr;
}
var arr = test();
arr[5]();              //返回5

在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,arr数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了

function s(){
	for(var i=0;i<5;i++){
		(function(n){   //嵌套函数用来创建本地作用域
			arr[i].onclick=function(){};
		})(i);   //将i的值赋值给局部变量
	}
}

此外,关于闭包还要注意一件事,那就是this,this是关键字,而不是变量(作用域链搜索的时候,只会搜索到变量对象而已),因此闭包里的this不同于外部函数的this,除非将this保存在一个闭包能够访问的变量中

var self  = this; //将this保存在变量中,以便嵌套的函数访问他

这个问题同样适用于arguments,虽然arguments不是关键字,但是每个函数都有自己的arguments对象,因此闭包无法直接访问外部函数的参数数组,除非外部函数也将参数数组保存下来

var outArguments = arguments;    //保存在变量中,以便嵌套的函数访问他

使用Function()构造函数时,有几点要注意

  • Function()构造函数允许JavaScript在运行中动态的创建并编译函数
  • 每次调用Function()构造函数都会解析函数体,并创建新的函数对象,因此在循环或者多次调用的函数中执行构造函数,会影响性能(相比之下,循环中的嵌套函数和函数定义表达式则不会每次执行都会重新编译)
  • 使用Function()构造函数创建的函数并不适用词法作用域,相反函数体代码的编译总是会在顶层(全局作用域)执行

    var a = "global";
    function test(){
    var a = "local";
    return new Function("alert(a)");
    }
    test()(); // 输出"global"

1. 私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量

私有变量包括函数的参数、局部变量和在函数内部定义的其他函数

function add(num1, num2){
	var sum = num1 + num2;
	return sum;    //3个私有变量:num1、num2、sum
}

这三个私有变量,在外部不可访问,但如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量,而利用这一点,就可以创建用于访问私有变量和函数的公有方法(特权方法)

第一种是在构造函数中定义特权方法

function MyObject(){
	//私有变量和私有函数
	var a = 10;
	function test(){
		return false;
	}
	//特权方法,唯一访问私有变量的方法
	this.public = function (){
		a++;
		return test();
	};
}

可以隐藏那些不应该被直接修改的数据

function Person(name){
	this.getName = function(){
		return name;
	};
	this.setName = function (value) {
		name = value;
	};
}

构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题

静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法

(function(){
	//私有变量和私有函数
	var privateVariable = 10;
	function privateFunction(){
		return false;
	}
	//构造函数
	MyObject = function(){
	};
	//公有/特权方法
	MyObject.prototype.publicMethod = function(){
		privateVariable++;
		return privateFunction();
	};
})();

这个模式在定义构造函数时并没有使用函数声明(函数声明只能创建局部函数),而是使用了函数表达式,并且省略了var(在严格模式下给未经声明的变量赋值会导致错误)

这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是
保存着对包含作用域的引用

以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终视具体需求而定

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个显明的不足之处

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式( module pattern)则是为单例创建私有变量和特权方法。所谓单例( singleton),指的就是只有一个实例的对象。按照惯例, JavaScript 是以对象字面量的方式来创建单例对象的

var singleton = {
	name : value,
	method : function () {
	//这里是方法的代码
	}
};

模块模式通过为单例添加私有变量和特权方法能够使其得到增强

var singleton = function(){
	 //私有变量和私有函数
	var privateVariable = 10;
	function privateFunction(){
		return false;
	}
	//特权/公有方法和属性
	return {
		publicProperty: true,
		publicMethod : function(){
			privateVariable++;
			return privateFunction();
		}
	};
}();

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于
这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有
变量时是非常有用的

var application = function(){
	//私有变量和函数
	var components = new Array();
	//初始化
	components.push(new BaseComponent());
	//公共
	return {
		getComponentCount : function(){
			return components.length;
		},
		registerComponent : function(component){
			if (typeof component == "object"){
				components.push(component);
			}
		}
	};
}();

简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式

增强的模块模式

增强的模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况

var singleton = function(){
	//私有变量和私有函数
	var privateVariable = 10;
	function privateFunction(){
		return false;
	}
	//创建对象
	var object = new CustomType();
	//添加特权/公有属性和方法
	object.publicProperty = true;
	object.publicMethod = function(){
		privateVariable++;
		return privateFunction();
	};
	//返回这个对象
	return object;
}();

九. 函数属性

函数是对象,因此也有属性与方法

函数继承的constructor属性返回函数的构造函数

function strong(){}
strong.constructor === Function;     //true

1. length属性

函数的length属性是只读的,表示函数在定义时指定的形参个数(实参的个数保存在函数内部实参对象中——arguments.length中)

function sum(num1, num2){ 
} 
alert(sum.length);    //2

2. prototype属性

每一个函数都包含一个prototype属性,指代一个原型对象

对于JavaScript中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过被各自对象的实例继承

当将函数用作构造函数的时候,新创建的对象就会该函数的原型对象上继承属性

原型属性是函数独有的属性

有一种函数没有prototype属性,详见bind()方法

3. name属性(非标准属性)

除IE外的浏览器给函数定义了一个非标准的name属性,对于函数声明,name等于函数名,对于函数表达式,name为空字符串

3. caller属性(非标准属性)

这个对象中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null

function inner(){ 
	//为了实现更松散的耦合
	//可以通过arguments.callee.caller来调用
	alert(inner.caller); 
} 
function outer(){ 
	inner(); 
} 
outer();   //输出outer()函数的源代码

严格模式下,访问callee和caller会导致错误

十. 函数方法

所有对象都具有继承自Object.prototype的toString()、toLocaleString()、和valueOf()等方法,函数也不例外,并且进行了重写

1. valueOf()方法

函数继承的valueOf()方法返回原函数(指向同一个函数)

2. toString()方法

函数继承的toString()方法返回函数的源代码的字符串形式(内置函数会返回一个"[native code]"的函数体)

所有在希望使用字符串的地方使用了函数时都会调用toString()方法

3. toLocaleString()方法

函数继承的toLocaleString()方法返回函数的源代码的字符串形式

4. call()和apply()方法

每个函数都包含非继承而来的call()和apply()方法

修改函数所在的作用域(即函数执行环境,亦即this的值)并调用函数,通过调用方法的形式来间接调用函数

这两个方法的第一个参数,都是要调用函数的母对象(调用上下文)

fun.call(obj);
fun.apply(obj);

相当于进行了如下操作

obj.m = fun;	
obj.m();
delete obj.m;

在非严格模式下,如果省略第一个参数,或者传入的第一个参数是null或者undefined,它们将被全局对象(浏览器环境下是window)所代替,如果传入的参数是基本类型值,则它们会被相应的基本包装类型所代替

而在严格模式下,如果省略第一个参数,this将是undefined;除此之外,所有的参数都会成为this的值,即使是null和undefined和基本类型值

var x="window";
var obj={
	  x:"obj",
	  m:function(){
	       alert(this.x);
	}
} 
function m(){
	alert(this.x)
}
obj.m.call(this)    //window
m.call(obj);     //obj

后续参数略有不同

  • 对于call()来说,后续的参数就是原函数的参数列表
  • 对于apply()来说,第二个参数是一个数组,原函数的参数的数组形式(可以是Array的实例,也可以是arguments对象)

使用call将类数组对象转换成数组

Array.prototype.slice.call(arguments,0)

快速求取数值数组的最大(最小值)

Math.max.apply(Math,[1,2,3]);  //3

小结

使用call()或apply()作用完全相同,选择哪个,取决于采取哪种方式给函数传递参数最方便(不传递参数的情况下,使用哪个方法都无所谓)

使用call()或apply()来修改作用域的最大好处,就是对象不需要与方法有任何耦合关系(即使一个函数不是对象的方法,也可以像对象方法一样被调用)

6. bind()方法——ECMAScript5

bind()方法同样也是将函数绑定至某个对象,但是不会调用该函数,而是返回含有新的作用域链的函数对象,调用返回的新函数,会把原始的函数当做对象的方法来调用,传入新函数的任何实参都会传入原始函数

第一个参数,都是要调用函数的母对象(调用上下文) ,后续参数可以是参数列表或者接受一个参数数组

function f(y){return this.x+y}
var o = {x:1};
var g = f.bind(o);
g(2);   //3

之前遇到过这种问题

window.onclick = obj.m; //m函数中的this不是obj,而是window
window.onclick = obj.m.bind(obj);  //this是obj

此外,bind()方法不仅仅是将函数绑定至一个对象,它还有很重要的一个作用:除了第一个实参之外,传入bind()的实参也会绑定至this,这是一种很常见的函数式编程技术——柯里化(currying)

function test(x,y){ alert(x+y); }
var news = test.bind(null,1);  //第一个参数绑定到1,新函数只期望接收一个参数
news(2);     //3

function test(y,z){ alert(this.x+y+z); }
var news = test.bind({x:1},2); 
news(3);     //6   this.x绑定到1,y绑定到2,z绑定到3

可以这样模拟bind

function bind(f, o) {
	if (f.bind){
		return f.bind(o);
	}else{
		return function() {  
			return f.apply(o, arguments);
		};
	}
}

更接近的bind()方法

if (!Function.prototype.bind) {
	Function.prototype.bind = function(o /*, args */) {
		var self = this, boundArgs = arguments;
		return function() {
			var args = [], i;
			for(i = 1; i < boundArgs.length; i++){
				args.push(boundArgs[i]);
			}
			for(i = 0; i < arguments.length; i++){
				args.push(arguments[i]);
			}
			return self.apply(o, args);
		};
	};
}

bind()方法返回的是一个闭包,尽管定义闭包的内部函数已经从外部函数中返回,而且调用这个闭包逻辑的时刻要在外部函数返回之后,但是闭包中仍能正确访问self和boundArgs两个变量

  • bind()方法所返回的函数的length属性是原函数的length属性减去使用bind()方法时,传入的2~n个参数的个数,但不会小于0
  • bind()方法所返回的函数可以用作构造函数,但是此时将忽略掉传入的this,原始函数就以构造函数的形式出现,所传入的实参会原封不动的传给原始函数
  • bind()方法所返回的函数不包含prototype属性(要知道,普通函数的prototype属性是不能删除的),返回的函数用作构造函数时所创建的对象从原始函数的prototype中继承属性

. 小结:内置全局函数汇总

转型函数

  • Number()
  • Boolean()
  • String()

数值相关函数

  • parseInt()
  • parseFloat()
  • isNaN()
  • isFinite()

编码函数

  • escape()
  • unescape()
  • encodeURI()
  • decodeURI()
  • encodeURIComponent()
  • decodeURIComponent()

其它

  • eval()

如果算上BOM的话,还有

  • setTimeout()
  • clearTimeout()
  • setInterval()
  • clearInterval()

你可能感兴趣的:(JavaScript)