JS原理 - 一些函数概念
匿名函数、自执行函数、高阶函数,柯里化
匿名函数
没有函数名的函数被称作匿名函数,形式为:function(){}
,匿名函数能让闭包更加简洁。
使用具名函数进行闭包:
function foo(){
let a = 0;
function child(){
a+=1;
console.log(a);
}
return child
}
const x = foo();
x(); // 1
x(); // 2
使用匿名函数进行闭包:
function foo(){
let a = 0;
return function(){
a+=1;
console.log(a);
}
}
const x = foo();
x(); // 1
x(); // 2
自执行函数
自执行函数,顾名思义,是会自己执行的函数,不需要额外的调用,形式为:(function(){})()
。
因为匿名函数拥有独立的词法作用域,避免外部访问自执行函数内的变量,所以在自执行函数内的变量不会污染全局作用域。
上面的匿名函数闭包可以使用自执行函数进行修改:
const x = (function(){
let a = 0;
return function(){
a+=1;
console.log(a);
}
})();
x(); // 1
x(); // 2
再来看一个古老的面试题:
for (var i = 0; i < 5; i++) {
setTimeout(function(){
console.log(i);
}, 1000);
}
因为宏任务、微任务的工作原理,会先执行完 for 循环之后再去执行定时器,所以上面的代码会打印出 5 5 5 5 5(这里不是 4 时因为,for循环最后的 i++ 不论条件是否成立,都会被执行)。
那么如果要让他打印出 0 1 2 3 4 需要怎么样修改代码?
首先 JS 中调用函数传递参数是按值传递的,传入的参数会被复制一份,然后再创建函数执行上下文,所以拿到的都是当时的值,同时产生一个闭包,将值保存下来。
for (var i = 0; i < 5; i++) {
function foo(a) {
setTimeout(function(){
console.log(a);
}, 1000);
};
foo(i)
}
// 使用立即执行函数去优化一下代码
for (var i = 0; i < 5; i++) {
(function (a) {
setTimeout(function(){
console.log(a);
}, 1000);
})(i);
}
// 这样也是可以的
for (var i = 0; i < 5; i++) {
setTimeout((function(a){
return function(){
console.log(a);
}
})(i), 1000);
}
再来看一个例子:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 16
f2(); // 16
f3(); // 16
这个理解很简单,往数组里 push 的是一个未执行的函数,所以最后执行出的结果都是 4 * 4 = 16,那如果想要让打印出来的结果分别是:1、4、9 ,需要如何修改函数?
第一种:将 push 方法包裹在自执行函数内,
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
(function(a){ // 按值传递
arr.push(function () {
return a * a;
});
})(i)
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
第二种:将 push 的回调函数用自执行函数进行嵌套,然后返回一个匿名函数,非常标准的闭包
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
高阶函数
满足下列条件之一的,可以被称作高阶函数:
函数可以作为参数传递
var arr = [1, 55, 23, 6]; arr.sort(function(a, b){ return a - b; })
函数可以作为返回值输出
function foo(){ let a = 0; function child(){ a+=1; console.log(a); } return child }
函数柯里化
柯里化是一种函数的转换,他是将一个函数从可调用的f(a,b,c)
转换为可调用的f(a)(b)(c)
,柯里化不会调用函数,只是对函数进行转换。
举个例子:现在有一个 add 函数
function add(a, b){
return a + b;
}
现在要对他实行柯里化,创建一个 curry 函数,将函数作为他的传入参数,然后返回一个函数:
function curry(f) {
return function(a) {
return function(b) {
return f(a, b);
};
};
}
const curryAdd = curry(add);
curryAdd(1)(2); // 3
可以看到,这里还是有闭包的思想存在,在调用curryAdd(1)
的时候,会保留他的词法作用域,返回一个 function(b)
。
那么看起来让函数更加复杂的柯里化有什么意义呢?
举个例子:如果一类需求的 x 都是1,如果不使用柯里化,那么每一次调用,都是add(1,n)
的形式,如果用柯里化的形式add(1)(n)
,add(1)
的结果可以存储下来,减少重复运算。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
increment(2); // 3
callee & caller
callee
在函数的内部,有两个特殊的对象:arguments 和 this。其中 arguments 是一个类似数组的对象,包含着传入函数的所有参数。
虽然 arguments 的主要用途是保存函数参数,但这个对象有一个属性 callee,该属性是一个指针,指向拥有这个 arguments 对象的函数。
例如下面的例子中,在函数内调用了自身,如果外层函数修改了名称,而其函数内部的名称没有做修改,那么就会出现错误,使用 callee 就能解决这个问题。
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * factorial(num-1);
}
}
// 修改为
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}
caller
caller是函数对象的一个属性,该属性保存着调用当前函数的函数的引用(指向当前函数的直接父函数)。
function a(){
b();
};
function b(){
console.info(b.caller);
};
a(); // ƒ a(){ b() }
callee 和 caller 也可以结合起来使用,例如下述代码能实现和上面代码一样的功能,且脱离了对函数名称的依赖。
function a(){
b();
};
function b(){
console.info(arguments.callee.caller);
};
a(); // ƒ a(){ b() }
JS原理 - 一些函数概念