定义函数
函数对象
因为函数也是对象,所以它可以像其他任何对象一样被使用。
- 函数可以保存在变量、对象和数组中
- 函数可以被当做参数被传递给其他函数
- 函数也可以再返回函数
而且,因为函数是方法,因此也可以拥有方法。
函数声明
function functionName(arg0,arg1,arg2){
//函数体
}
ES6标准化了name属性,通过这个属性可以访问到函数的名字:
functionName.name //"functionName";
函数声明提升
函数声明的一个重要特征是函数声明提升,即在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。
sayHi()
function sayHi(){
alert("hi");
}
函数表达式
var fucntionName = function(arg0,arg1,arg2){
//函数体
};
- 看起来像常规的变量赋值语句,即创建一个函数并将它赋值给变量functionName。
- 这种情况下创建的函数为匿名函数,因为function关键字后面没有标识符。
- 匿名函数的name属性为空字符串。
- 函数表达式的name属性为该函数所赋值的变量名,此处即为functionName
函数表达式并不会被提升,必须在使用前先赋值:
sayHi(); //TypeError!!函数还不存在
var sayHi = function(){
alert("hi");
}
函数提升有可能带来意想不到的效果:
即一个普通块内部的函数声明通常会被提升到所在作用域的顶部,因此这个过程不会像西面的代码暗示的那样可以通过条件判断所控制:
if(condition){
function sayHi(){
alert("hi");
}
}else{
function sayHi(){
alert("yo");
}
}
由于函数声明提升,在ECMAScript中并不会因为condition的值而定义不同的函数,不同浏览器处理的结果不一样。
但是在ES6的严格模式中支持在代码块内部声明函数,此时函数为块级声明,可以在定义所在的代码块内部被访问。此时的函数为块级函数。
块级函数会被提升到所在的代码块的顶部。
函数表达式在ES6之前的版本中就没什么问题:
var sayHi;
if(condition){
sayHi = function(){
alert("hi");
}
}else{
sayHi = function(){
alert("yo");
}
}
这个例子不会有什么例外,不同的函数会根据condition的值而被赋给sayHi。
有关声明提升还有以下需要注意的:
- 每个作用域都会进行提升操作,包括全局作用域与每个函数作用域内部。
- 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。
- 变量声明和函数声明都会被提升,但是函数会被首先提升,然后才是变量。
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
这个代码片段会被引擎理解为如下模式:
function foo(){
console.log(1);
}
foo(); //1
foo = function(){
console.log(2);
}
尽管var foo 出现在function foo()……的声明之前,但是它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
尽管重复的var声明会被忽略掉,但是出现在后面的函数声明还是可以覆盖前面的:
foo(); //3
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
function foo(){
console.log(3);
}
递归
递归函数是在一个函数通过名字调用自身的情况下构成的:
function factorial(num){
if(num<=1){
return 1;
}else{
return num*factorial(num-1);
}
}
这是一个经典的递归阶乘函数,但由于函数与函数名紧耦合,可能会产生问题:
var anotherFactorial = factorial;
factorial = null;
anotherFactorial(4); //出错!!
我们知道,可以利用arguments.callee来解决这个问题。arguments.callee是一个指向正在执行的函数的指针,可以用它来实现对函数的递归调用:
function factorial(num){
if(num<=1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}
用arguments.callee来代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用arguments.callee总比使用函数名更保险。
但在严格模式下,不能通过脚本访问arguments.callee,会导致错误。
可以使用命名函数表达式来解决这种问题:
var factorial = (fucntion f(num){
if(num<=1){
return 1;
}else{
return num * f(num-1);
}
});
以上代码创建了一个名为f的命名函数表达式,然后将它赋值给变量factorial。即使把函数f赋值给另一个变量,函数f的名字依然有效,递归依然可以完成。
闭包
当函数可以记住并访问所在的作用域(全局作用域除外)时,就产生了闭包,即使函数是在当前作用域之外执行。
闭包是一个栈框架,当函数开始执行时被分配,并且在函数返回之后不被释放。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //2
- 函数bar可以访问到foo的内部作用域中的a。
- 在foo()执行后,其返回值(即函数bar)赋值给baz变量并调用baz(),其实就是通过不同的标识符引用调用了内部的bar函数
- bar函数在其自己定义的词法作用域之外被执行。
- 在foo()执行过后,其整个内部作用域并没有被销毁,这是由于闭包的存在。
bar()依旧持有对该作用域的引用,这个引用就是闭包。
这个函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
再来看一个例子:
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000);
}
wait("hello!closure")
将一个内部函数timer传递给setTimeout。timer具有涵盖wait()作用域的闭包。
wait()执行1000毫秒后,它的内部作用域并不会消失,timer依然保有wait()作用域的闭包。
变量对象与作用域链
这部分可以看js学习笔记:执行环境与作用域 来复习
变量对象:每个执行环境都会有一个表示变量的对象,存放在该环境中定义的所有变量和函数。
- 全局环境的变量对象始终存在
- 像compare函数这样的局部环境的变量对象,只在函数执行的过程中存在。
作用域链:当代码在一个环境中执行时,会创建该环境变量对象的一个作用域链。本质上是一个指向变量对象的指针列表,只引用但不实际包含变量对象。(最前端是当前变量对象,最后端是全局变量对象)
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。
一般在函数执行完毕后,局部活动对象就会被销毁。但在闭包中不同:
在另一个函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议只在绝对必要的时刻使用闭包。
闭包与变量
作用域链的这种配置机制引出了一个副作用:即闭包只能取得外部函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for(var i=0;i<10;i++){
result[i] = function(){
return i;
}
}
return result;
}
这个函数会返回一个函数数组。表面上看每个函数都应该返回自己的索引值,但最后每个函数运行的结果都是10,10,10,10……因为每个函数的作用域链中都保存着createFunctions函数的活动对象,所以他们引用的是同一个变量i。当createFunctions函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以每个函数内部i的值都是10.
解决方法:
function createFunctions(){
var result = new Array();
for(var i=0;i<10;i++){
result[i] = (function(num){
return function(){
return num;
};
}(i));
}
return result;
}
- 这次没有直接把闭包赋值给数组,而是定义了一个立即执行的匿名函数IIFE,并将该函数执行的结果赋给数组。
- 这个立即执行函数有一个参数num,在每次调用匿名函数时,都传入了变量i,由于函数参数是按值传递的,因此就会将变量i的当前值复制给num。
- 在这个匿名函数内部,又创建并返回了一个访问num的闭包
- 这样一来,result数组中每个函数就是:
function(){
return num;
};
并且每个函数都有自己的num变量,可以返回不同的数值。
也可以使用let来解决这种问题:
function createFunctions(){
var result = new Array();
for(let i=0;i<10;i++){
result[i] = function(){
return i;
}
}
return result;
}
使用IIFE的方法是在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域。因此使用let变量可以在每次迭代中生成一个新的块作用域,绑定当时的i值。(在每次迭代中,let都会创建一个新的同名变量并对其进行初始化,因此每次迭代都会是一个新的块级作用域)
this对象
this对象是在运行时基于函数的执行环境绑定的:
- 全局函数中:this等于window
当函数被作为某个对象的方法调用时,this为那个对象
但是匿名函数的执行环境具有全局性,因此其this对象通常指向window
var name = "the window";
var object = {
name: "My Object",
getNameFunc : function(){
return function(){
return this.name;
}
}
};
object.getNameFunc()(); //the window
- getNameFunc返回一个匿名函数,因此想要执行这个函数需要再加个圆括号:object.getNameFunc( )( )
- 每个函数在被调用时都会自动取得this和arguments这两个变量,但内部函数在搜索这两个变量时只会搜索到其活动对象为止,永远不可能直接访问外部函数的这两个变量。因此此时内部匿名函数的this为window,就会返回window的name
解决方法:把外部作用域中的this对象保存在一个闭包能访问到的变量里
var name = "the window";
var object = {
name: "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
}
}
};
object.getNameFunc()(); //My Object
- 在定义闭包之前把this对象(object对象)赋值给that
- 闭包可以访问that变量,即使在函数返回后,that也依然引用着object,所以调用这个匿名函数可以返回“My Object”
this和arguments对象都存在这样的问题,arguments也要做同样处理
更多有关this的内容可以参考JS学习笔记:this
模仿块级作用域
js没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。
function outputNumbers(count){
for(var i=0;i<count;i++){
alert(i);
}
alert(i); //可以访问到i
}
立即执行函数表达式IIFE可以用来模仿块级作用域(私有作用域)并避免这个问题:
(function(){
//这里是块级作用域
})();
- 以上代码定义并立即调用了一个匿名函数
- 将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式
- 紧随函数表达式后面的另一对圆括号会立即调用这个函数
区分函数声明和函数表达式最简单的方法就是看function关键字的位置:如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
另一种形式也是完全正确的:
(function(){……}())
即最后的括号被包含在了外面的括号中。
以上两种形式在功能上是一致的,选择哪个全凭个人喜好。
IIFE另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去:
var a = 2;
(fucntion IIFE(global){
var a = 3;
console.log(a); //3
console.log(global.a); //2
})(window);
我们将window对象的引用传递进去,但将参数命名为global。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。
function outputNumbers(count){
(function(){
for(var i=0;i<count;i++){
alert(i);
}
})();
alert(i); //错误!!
}
- 以上代码中,我们插入了一个私有作用域。在这个匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用。
- 但是这个私有作用域中能够访问变量count,是因为这个匿名函数是一个闭包。
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。
一般来说,我们都应该尽量少向全局作用域中添加变量和函数,以免导致命名冲突。通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。
这种做法也可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕就可以立即销毁其作用域链了。
当然在ES6中已经支持了块级作用域,使用let与const声明变量。
私有变量
- 任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
- 私有变量包括
- 函数的参数
- 局部变量
- 在函数内部定义的其它函数。
function add(num1,num2){
var sum = num1 + num2;
return sum;
}
在这个函数内部,有3个私有变量:num1、num2、sum。在函数内部可以访问到这几个变量,但在函数外部则不能访问他们。
但如果在函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。利用这一点,就可以创建用于访问私有变量的公有方法。
特权方法:有权访问私有变量和私有函数的公有方法。
有两种在对象上创建特权方法的方式:
在构造函数中定义特权方法:
function MyObject(){
//私有变量和函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function(){
privateVariable ++;
return privateFunction();
}
}
- 这个模式在构造函数内部定义了所有私有变量和函数,然后创建了能够访问这些私有成员的特权方法。
- 特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
这个例子中,变量privateVariable和函数privateFunction只能通过特权方法publicMethod来访问。在创建了MyObject的实例后,除了使用publicMethod这一个途径外,没有任何办法可以直接访问privateVariable和privateFunction。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据:
function Person(name){
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
var person = new Person("nicholas");
person.getName(); //"nicholas"
person.setName("greg");
person.getName(); //"greg"
- 这个构造函数中的两个特权方法都可以在构造函数外部使用,而且都有权访问私有变量name。(作为闭包通过作用域链访问name)
- 但在Person构造函数外部,没有任何方法能访问到name。
- 私有变量name在Person的每一个实例中都不相同,因为每次调用构造函数都会创建这两个方法。
在构造函数中定义特权方法有一个缺点:必须使用构造函数模式来达到这个目的,但构造函数模式是有缺陷的。因此考虑使用静态私有变量来实现特权方法。
静态私有变量
在私有作用域中定义私有变量或函数:
(function(){
//私有变量和函数
var privateVariable = 10;
function privateFunction(){
return false;
//构造函数
MyObject = function(){};
//特权方法
MyObject.prototype.publicMethod = function(){
privateVariable ++;
return privateFunction();
}
})();
- 这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法
- 公有方法是在原型上定义的,这一点体现了典型的原型模式。
- 这个模式在定义构造函数时没有使用函数声明,而是函数表达式。因为函数声明只能创建局部函数,我们也没在声明MyObject时使用var关键字,由于初始化未经声明的变量总是会创建一个全局变量,因此我们得到了全局变量MyObject,能够在私有作用域外被访问到。
但是严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别:私有变量和函数是由实例共享的。
由于特权方法实在原型上定义的,因此所有实例都使用同一个函数。而这个特权函数作为一个闭包,总是保存着对包含作用域的引用。
(function(){
var name = "";
Person = function(value){
name = value;
}
Person.prototype.getName = function(){
return name;
}
Person.prototype.setName = function(value){
name = value;
}
})();
var person1 = new Person("nicholas");
person1.getName(); //"nicholas"
person1.setName("greg");
person1.getName(); //"greg"
var person2 = new Person("michael");
person1.getName(); //"michael"
person2.getName(); //"michael"
这种情况下,name变成了一个静态的、由所有实例共享的属性。
也就是说,在一个实例上调用setName()会影响所有实例。而调用setName()或新建一个Person实例都会赋予name属性一个新值。结果就是所有实例都会返回相同的值。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量还是静态私有变量,就要看具体需求而定。
使用闭包和私有变量有一个明显的不足之处:多查找作用域链中的一个层次,就会在一定程度上影响查找速度。
模块模式(重要!!)
我们可以使用函数和闭包来创造模块。
模块是一个提供接口却隐藏状态与实现的函数或对象。
前面的模式是用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法。
单例:只有一个实例的对象
一般来说js是以对象字面量的方式来创建单例对象的:
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);
}
}
}
}());
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。
模块模式创建的每个实例都是Object的实例,因为最终要通过一个对象字面量来表示它。
增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。
这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增强的情况。
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;
}()
比如说如果之前那个例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码:
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
var app = new BaseComponent();
//公共接口
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if(typeof component == "object"){
components.push(component);
}
};
return app;
}()
这个例子与模块模式中例子的区别就是返回的单例对象必需是BaseComponent的实例,因此需要在返回对象前将其初始化为BaseComponent的实例,并添加公有方法。