函数的使用
1)普通用法
2)当值传递:也就是作为值来传递
3)函数作为返回值
以上就是函数常见的几种用法。
后面这两种情况就会涉及到闭包,因此还会详细讨论
1 2 3 4 5 6 7 8 9
| 函数作为返回值 function fn(){ var min = 10; return function compare(x){ if(x>max){ console.log(x); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| 函数作为值传递 function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz); }
function bar(fn){ fn(); }; foo();
|
上下文执行环境和作用域
词法作用域(静态作用域)
词法作用域关心的是函数和作用域是如何声明以及在何处声明,而并不是动态作用域
1 2 3 4 5 6 7 8 9 10 11 12
| function foo() { console.log(a); }
function bar() { var a = 3; foo(); }
var a = 2;
bar ();
|
(这里打印出的是全局的2,可见,虽然foo()
是在bar()
中被调用的,但是由于foo()的作用域链是 foo()→全局
,因此,并不会搜索到bar()
中的a,而如果是动态作用域,就会搜索到bar中的a了。)
上下文环境 EC
1 2 3 4 5 6 7 8 9 10 11
| var a = 10; var fn; var bar = function(x){ var b = 5; fn(x+b); }; fn = function(y){ var c = 5; console.log(y+c); }; bar(10);
|
1 2 3
| fn-上下文环境 bar-上下文环境 bar-上下文环境 bar-上下文环境 全局上下文环境 -> 全局上下文环境 -> 全局上下文环境 -> 全局上下文环境 -> 全局上下文环境
|
一个完整的闭环,进栈设置为活动状态,也就是保存其上下文数据,出栈即被销毁其上下文数据,干脆利落,但是,后面要说到的闭包就不是这么干脆的说销毁就销毁了(垃圾回收)。
而对于上下文环境
EC
- 变量对象 VO :(Variable Object)保存此EC中涉及到的变量。
- 作用域链 ScopeChain: 此EC中的VO与其他EC中的VO的关联关系(能否访问到)
- this: 在EC被创建时,会确定this的指向
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。
改变或延长作用域
- 改变可以用 apply(),call(),bind()这些函数
- 延长可以用 try-catch 语句的catch块,with语句
块级作用域
对于 ES5
, 没有块级作用域,也就是花括号括起来的作用域,javascript
除了全局作用域之外,只有函数可以创建的作用域 所以,在if,for语句中要特别注意。
比如:
1 2 3 4 5
| var a = 10; if(a>5){ a = 4; } console,log(a);
|
闭包
因为作用域链,外部不能访问内部的变量和方法,这时我们就需要通过闭包,返回内部的方法和变量给外部,从而就形成了一个闭包。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function foo() { var a = 2;
function bar() { console.log(a); }
return bar; }
var baz = foo();
baz();
|
上面这段代码,是函数作为返回值的情况,当foo()执行后,将bar函数赋值给baz,本来应该就跟foo没什么关系了,其上下文就应该会被销毁了,但是因为bar需要打印出a的值,而根据词法作用域,它要在它的作用域链(bar→foo→全局)中寻找,因此可以在foo中找到a,所以,foo()的内部作用域依然存在,因为bar()持有着对该作用域的引用,使得该作用域一直存活,以供bar()在之后的任何时间进行引用。 而这个引用就叫闭包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var fn; function foo() { var a = 2;
function baz() { console.log(a); }
fn = baz; }
function bar() { fn(); }
foo();
bar();
|
接下来,看看for循环的坑
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function fn(){ var arr = new Array; var i; for(i=0;i<10;i++){ arr[i] = function(){ return i; } } return arr; } var array = fn(); for(var j=0; j<10;j++){ console.log(array[j]()); }
|
fn函数执行后,返回了一个函数数组,这个数组包含10个会return i的函数,接下来让array执行,这时候,会回去fn函数查找i,而此时i已经是10了,10个函数都回到fn函数的去找i,搜索到的是同一个i,都是10. 那么,有什么办法来解决这个问题呢?
解决的方法:(比较多)
1.我们可以通过创建另一个匿名函数强制将每次的i的值保存在一个单独的闭包中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function fn(){ var arr = new Array; var i; for(i=0;i<10;i++){ arr[i] = function(num){ return function(){ return num; }; }(i); } return arr; } var array = fn(); for(var j=0; j<10;j++){ console.log(array[j]()); }
|
(
这里返回的数组仍为函数数组,但是每个函数都又返回了一个函数,这个返回的函数return的num是在arr函数中找的,又因为每个arr函数都保存了当时的i,也就是i通过传参保存进去。所以他们能有不同的闭包环境,每个闭包都保存了各自的i。)
2.创建一个匿名函数并返回该匿名函数的立即执行
1 2 3 4 5 6 7 8 9 10 11 12 13
| function fn(){ var arr = new Array; var i; for(i=0;i<10;i++){ arr[i] = (function(num){ return num; })(i); } return arr; } var array = fn(); for(var j=0; j<10;j++){ console.log(array[j]); }
|
3.使用ES6新特性let
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function fn(){ var arr = new Array; for(let i=0;i<10;i++){ arr[i] = function(){ return i; } } return arr; } var array = fn(); for(var j=0; j<10;j++){ console.log(array[j]()); }
|
同样,可以参考高程中的相似例子(settimeout和循环与闭包的使用,改写使得输出10个10)
接下来,还有一个例子:
1 2 3 4 5
| for (var i=1; i<=5; i++) { setTimeout(function timer() { console.log(i); }, 1000); }
|
因为for循环不会创建块级作用域,5次执行的回调函数都被封闭在一个共享的全局作用域中了,因此实际上只有一个i。循环结构让我们误以为背后还有更复杂的机制在起作用,但其实没有。
解决方案代码:
1 2 3 4 5 6 7 8
| for (var i=1; i<=5; i++) { (function() { var j = i; setTimeout(function timer() { console.log(j); }, 1000); })(); }
|
也相当于:
1 2 3 4 5 6 7
| for (var i=1; i<=5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j*1000); })(i); }
|
IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
(IIFE即Immediately Invoked Function Expression,立即执行函数表达式。)
还可以更简单:
1 2 3 4 5
| for (var i=1; i<=5; i++) { setTimeout(function timer(i) { console.log(i); }, 10, i); }
|
闭包的应用
模块模式
闭包的强大威力:应用于模块模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function CoolModule() { var something = 'cool'; var another = [1, 2, 3];
var doSomething() { console.log(something); }
function doAnother() { console.log(another.join('!')); }
return { doSomething: doSomething, doAnother: doAnother } }
var foo = CoolModule();
foo.doSomething(); foo.doAnother();
|
每次调用都会创建一个新的模块实例。
函数柯里化
函数的柯里化,是指将本来接收多个参数的函数,变换为接收更少参数的函数,其中减少的参数,被设为了固定值。柯里化函数的使用,可以在函数被多次调用时,用来减少相同参数的重复输入, 相当于是对原函数进行了分步传值,当且仅当函数所需的所有参数都传入了对应值时,函数才会返回最终的处理结果,否则只会返回新的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Curry(fn) { var stored_args = Array.prototype.slice.call(arguments, 1) return function () { var new_args = Array.prototype.slice.call(arguments) args = stored_args.concat(new_args) return fn.apply(null, args) } }
var doSomething = function (_do, something) { console.log(_do+ ',' + something) } var newDoSomething = Curry(doSomething, 'hello') newDoSomething('Jack') newDoSomething('Tom')
|
创建单例模式
单例模式的定义是产生一个类的唯一实例
1 2 3 4 5 6 7 8 9 10 11 12
| var getSinleInstance = (function () { function Foo() { this.name = 'Jack' } var instance = new Foo(); return function () { return instance; } }()) var instance1 = getSinleInstance(); var instance2 = getSinleInstance(); console.log(instance1 === instance2)
|
原函数形成一个闭包,保存着instance这个被new 出来的实例,因此每一次都是返回相同的instance实例
另一个例子:
1 2 3 4 5 6 7 8 9 10 11 12
| var singleton = function( fn ){ var result; return function(){ return result || ( result = fn .apply( this, arguments ) ); } } var createMask = singleton( function(){ return document.body.appendChild( document.createElement('div') ); })
|
这里也是一个单例模式,这个例子中节点创建一次后就不再创建,因此可以大大节约Dom的开销。(这种方式其实叫桥接模式.)然而它始终还是需要一个变量result来寄存div的引用.遗憾的是js的函数式特性还不足以完全的消除声明和语句.
(最后,欢迎大家围观指正)