总结
第12章函数进阶
在第5章中我们已经学习过了函数,不过那些只是表面的一些东西。在JavaScript中,函数是一等 公民,可以像其他值一样作为参数来进行传递。这也给JavaScript这门语言带来了许多其他语言 所没有的特性。
本章将学习如下内容:
・立即执行的函数表达式
・递归函数
•闭包
・高阶函数
・函数柯里化
・函数式编程介绍
・函数的节流与防抖
・变量的初始化
12-1立即执行函数表达式
立即执行的函数表达式的英文全称为Immediately Invoked Function Expression,简称就为
IIFE。这是一个如它名字所示的那样,在定义后就会被立即调用的函数。
我们在调用函数的时候需要加上一对括号,IIFE同样如此。除此之外,我们还需要将函数变为一 个表达式,只需要将整个函数的声明放进括号里面就可以实现。具体的语法如下:
(function(){
//函数体
})()
接下来我们来看一个具体的示例:
(function(){ console.log("Hello");
})()
// Hello
IIFE可以在执行一个任务的同时,将所有的变量都封装到函数的作用域里面,从而保证了全局的 命名空间不会被很多变量名污染。
这里我可以举一个简单的例子,在以前我们要交换两个数的时候,往往需要声明第三个临时变量
temp
|注:从ES6开始已经不需要这么做了,直接使用结构就可以交换两个数了
let a = 3,b = 5;
let temp = a;
a = b;
b = temp;
console.log(a);//5
console.log(b);//3 console.log(temp);//3
这样虽然我们的两个变量被交换了,但是存在一个问题,那就是我们在全局环境下也存在了一个 temp变量,这就可以被称之为污染了全局环境。所以我们可以使用IIFE来解决该问题,如下:
let a = 3,b = 5;
(function(a,b){
let temp = a;
a = b;
b = temp;
})(a,b)
console.log(a);//3
console.log(b);//5
console.log(temp);//撿错
这是一个非常方便的功能,特别是有些时候我们在初始化一些信息时需要一些变量的帮助,但是 这些变量除了初始化之后就再也不会用了,那么这个时候我们就可以考虑使用IIFE来进行初始 化,这样不会污染到全局环境。
( function(){
let days =["星期天",”星期一","星期二",”星期三",”星期四",”星期五",”星期六"];
let date = new Date();
let today = [date.toLocaleDateString(),days[date.getDay()]]; console.log('今天是${today[0]}, ${today[1]},欢迎你回来! ');
})()
//今天是2017-12-20,星期三,欢迎你回来!
这里我们只是想要输出一条欢迎信息,附上当天的日期和星期几,但是有一个很尴尬的地方在于 上面定义的这些变量我们都只使用一次,后面就不会再用了,所以这个时候我们也是可以考虑使 用IIFE来避免这些无用的变量声明。
通过IIFE,我们可以对我们的代码进行分块。并且块与块之间不会互相影响,哪怕有同名的变量 也没问题,因为IIFE也是函数,在函数内部声明的变量是一个局部变量,示例如下:
(function(){
//block A
let name = "xiejie";
console.log('my name is ${name}');
})();
( function(){
//block B
let name = "song";
console.log('my name is ${name}');
})();
// my name is xiejie
// my name is song
在var流行的时代,JS是没有块作用域的。什么叫做块作用域呢?目前我们所知的作用域大概 有两种:全局作用域和函数作用域。其中,全局作用域是指声明的变量可在当前环境的任何地方 使用。函数作用域则只能在当前函数所创造的环境中使用。块级作用域是指每个代码块也可以有 自己的作用域,比如在if块中声明一个变量,就只能在当前代码块中使用,外面无法使用。而 用var声明的变量是不存在块级作用域的,所以即使在if块中用var声明变量,它也能在外 部的函数或者全局作用域中使用。
function show(valid){
if(valid){
var a = 100;
}
console.log('a:',a);
}
show(true); // 输出a的值为 100
这个例子中,a变量是在if块中声明,但是它的外部仍然能输出它的结果。
解决这个问题有两种方法,第一:使用ES6中的let关键字声明变量,这样它就有块级作用域。 第二:使用IIFE,示例如下:
function show(valid){
if(valid){
( function(){
var a = 100;
})();
}
console.log('a:',a);
}
show(true); // 报错:a is not defined
当然,只要浏览器支持,建立尽量使用let的方式来声明变量。
12-2变量初始化
12-2-1执行上下文
在ECMAScript中代码的运行环境分为以下三种:
・全局级别的代码:这是默认的代码运行环境,一旦代码被载入,JS引擎最先进入的就是这个 环境
・函数级别的代码:当执行一个函数时,运行函数体中的代码。
・EvaI级别的代码:在EvaI函数内运行的代码。
为了便于理解,我们可以将"执行上下文"粗略的看做是当前代码的运行环境或者说是作用域。下 面我们来看一个例子,其中包括了全局以及函数级别的执行上下文,如下:
let one = "Hello";
let test = function(){
let two = "Lucy",three = "Bill";
let test2 = function(){
console.log(one,two);
}
let test3 = function(){
console.log(one,three);
}
test2();
test3();
}
test();
上面这段代码,本身是没有什么意义的,我们主要是要使用这段代码来分析一下里面存在多少个 上下文。在上面的代码中,一共存在4个上下文。一个全局上下文,一个test函数上下文,一个 test2函数上下文和一个test3函数上下文。
通过上面的例子,我们就可以得出下面的结论:
・不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也 就是说,我们可以在test的上下文中访问到全局上下文中的o ne变量,当然在函数test2或者 test3中同样可以访问到该变量。
・至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出 —个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变 量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。
在上述例子中,内部的函数可以访问到外部上下文中的声明的变量,反之则行不通。那么,这到 底是什么原因呢?引擎内部是如何处理的呢?这需要了解执行上下文堆栈。
执行上下文堆栈
JS引擎的工作方式是单线程的。也就是说,某一个时候只有唯一的一个事件是处于被激活的,其 他的事件都是被放入队列中,等待被处理的。下面的示例图就描述了这样一个堆栈,如下:
我们已经知道,当JS代码文件被JS引擎载入后,默认最先进入的是一个全局的执行上下文。当 在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创 建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。
JS引擎总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然 后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回 到全局的上下文。
来看下面这段代码,分析其一共有多少个上下文:
(function foo(i){ if(i==3)
{
return;
}
else{
console.log(i);
foo(++i);
}
})(0);
//全局上下文
//函数上下文0
//函数上下文1
//函数上下文2
//函数上下文
上述foo被声明后,通过0运算符强制直接运行了。函数代码就是调用了其自身4次,每次是局部 变量i增加1。每次foo函数被自身调用时,就会有一个新的执行上下文被创建。每当一个上下文执 行完毕,该上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。所以在本段代 码中一共存在了 5个不同的执行上下文。
由此可见,对于执行上下文这个抽象的概念,可以归纳为以下几点:
・单线程
•同步执行
・唯一的一个全局上下文
・函数的执行上下文的个数没有限制
・每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如 此。
12-2-2函数上下文的建立与激活
我们现在已经知道,每当我们调用一个函数时,一个新的执行上下文就会被创建出来。然而,在 js引擎的内部,这个上下文的创建过程具体分为两个阶段,分别是建立阶段和代码执行阶段。这 两个阶段要做的事儿也不一样。
建立阶段:发生在当调用一个函数,但是在执行函数体内的具体代码之前
•建立变量对象(arguments对象,形式参数,函数和局部变量)
•初始化作用域链
・确定上下文中this的指向对象
代码执行阶段:发生在具体开始执行函数体内的代码的时候
・执行函数体内的每一句代码
我们将建立阶段称之为函数上下文的建立,将代码执行阶段称之为函数上下文的激活。
变量对象
在上面介绍函数两个阶段中的建立阶段时,提到了一个词,叫做变量对象。这其实是将整个上下 文看做是一个对象以后得到的一个词语。具体来讲,我们可以将整个函数上下文看做是一个对 象,那么既然是对象,对象就应该有相应的属性。对于我们的执行上下文来说,有如下的三个属 性:
executionContextObj = {
variableObject : {}, //变量对象,里面包含arguments对象,形式参数,函数和局部变量 scopeChain : {},//作用域链,包含内部上下文所有变量对象的列表 this : {}//上下文中this的指向对象
}
可以看到,这里我们的执行上下文对象有3个属性,分别是变量对象,作用域链以及this,这里我 们重点来看一下变量对象里面所拥有的东西。
在函数的建立阶段,首先会建立arguments对象。然后确定形式参数,检查当然上下文中的函数 声明,每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就指向该 函数在内存中的地址的一个引用。如果上述函数名已经存在于variableObject(简称V0)下面,那 么对应的属性值会被新的引用给覆盖。最后,是确定当前上下文中的局部变量,如果遇到和函数 名同名的变量,则会忽略该变量。
好,接下来我们来通过一个实际的例子来演示函数的这两个阶段以及变量对象是如何变化的。
let foo = function(i){
var a = "Hello";
var b = function privateB(){};
function c(){}
}
foo(10);
首先在建立阶段的变量对象如下:
fooExecutionContext = {
variavleObject : {
ar guments : {0 : 10,length : 1}, // 确定arguments 对象
i :