闭包与高阶函数

1.作用域

理解闭包需要先弄明白两个概念,作用域链和变量生存周期

  1. 作用域链

你不知道的 js 中有个形象的比喻

把程序中的嵌套作用域链比作一座大楼。如果一楼代表当前执行作用域。顶层代表全局作用域。

当访问变量时,先在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域)

  1. 函数作用域

JavaScript 具有基于函数的作用域,当我们把代码片段外部添加函数,可以将内部的变量和函数定义隐藏起来,外部作用域无法访问函数内部的任何内容

例如下面这段代码:

function foo() {
  var a = 2;
  console.log(a); // 2 当前作用域
}
foo();
console.log(a); // ReferenceError: a is not defined
var a = 1;
function foo() {
  var a = 2;
  console.log(a); // 2 当前作用域
}
foo(); // foo执行后出栈,就像这个函数从来不存在一样
console.log(a); // 1 全局作用域

foo 拥有内部变量,这样看似实现了封装的目的,但是这么做有几个问题,后面会有更好的方案

  • 要额外创建 foo 函数
  • 污染当前作用域(这段代码之前或之后存在其他 foo 变量,可能导致异常)
  • 必须显式调用 foo 函数

IIFE 立即执行函数表达式

继续刚才 foo 封装的话题,有没有这样一种函数,没有名称,而且还能自执行呢,这样就解决了以上的几个问题。既然这么问了肯定有。。

例如下面这段代码:

var a = 1;
(function foo() {
  var a = 2;
  console.log('我来自内部', foo); // 我来自内部 ƒ foo() {...}
  console.log(a); // 2 局部变量
})();
console.log(a); // 1 全局变量
console.log(foo); // ReferenceError: foo is not defined foo 去哪了???

what?发生了什么,内部变量访问不了还说得过去,怎么 foo 函数也没了?想搞明白这个,先了解一下函数声明和函数表达式的区别,仔细看下函数表达式的作用域绑定就清楚了。

函数表达式:常见的以(!±~等开头的,都是函数表达式。箭头函数也是。

function 不是声明的第一个词
作用域绑定 绑定在函数表达式自身的函数中
function 是否命名都一样, 这里只是为了输出展示,当然如果没有名称,内部可以通过 arguments.callee 来获取当前执行的函数
比如 (function foo(){ … }) foo 只能在… 所代表的位置中被访问,外部作用域则不行

// 这些都是函数表达式
!(function() {})();
+(function() {})();
-(function() {})();
~(function() {})();
void (function() {})();
new (function() {})();

(function() {}.call());
(function() {}.call());
// 这两种方式,会有不好的情况:如果括号前面有东西
a(function() {}).call();
//等价于
a().call();

函数声明:这个就不用介绍了,平时一直在用

function 是声明中的第一个词 作用域绑定 绑定在函数所在的作用域 比如 function foo() {} foo
被绑定在声明时的作用域 例子中是全局 也就是 window

块作用域

JavaScript 没有块作用域?

除 JavaScript 外的很多编程语言都支持块作用域,那么 JavaScript 没有块作用域吗?

可以理解为 JavaScript 中,只有函数作用域,全局作用域,没有块作用域。特殊情况除外(后面会说到 try cache/let/const)

例如下面这段代码:

// 同步执行 依次输出0-9
console.log('变量提升 ', i); // 变量提升 undefined
for (var i = 0; i < 10; i++) {
  console.log(i);
}
console.log('循环执行完毕 全局变量 ', i); // 循环执行完毕 全局变量 10

上面这个比较好理解,因为代码是同步的,执行顺序和书写顺序一致,从上到下依次执行。下面我们增加使用定时器模拟异步操作

// 异步执行 输出10次10
for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    // console执行时 for循环已经执行完毕 此时i=10
    console.log(i);
  }, 0);
}
console.log('循环执行完毕 全局变量 ', i); // 循环执行完毕 全局变量 10

异步代码和我们平时的认知不同,执行顺序和书写不一致。我们简单分析下执行过程:

执行 for 循环 遇到异步代码 依次在事件队列尾部添加回调。这里添加 10 次 执行最后一句 console
同步任务执行完毕,就去事件队列中查找需要执行的事件,这里是 setTimeout 的回调,然后同步的执行,输出全局变量
i,此时循环已经执行完毕,i 是 10,所以输出 10 个 10

JavaScript 实现块作用域

try/catch

try {
  undefined(); // 执行一个非法操作来强制制造一个异常
} catch (err) {
  console.log(err); // 能够正常执行!
}

console.log(err); // ReferenceError: err not found

let/const
es5 引入了 let/const,用于创建块作用域,而且声明的变量不再提升作用域。还是以经典的 for 循环为例:

// 异步执行 依次输出0-9
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}
// 这个的执行结果是:0,1,2,..9

// 这样也行
for (var i = 0; i < 10; i++) {
  let j = i; // 这里创建了块级作用域
  setTimeout(() => {
    console.log(j);
  }, 0);
}

同样是异步执行,为啥这里就不是 10?

循环内部使用 let 创建了块级作用域,变量 i 查找时,使用块级作用域内的值。 因为 setTimeout 内部函数引用了变量
i,循环执行完毕,定时器执行前,变量 i 还没有被回收。

IIFE 立即执行函数(模拟块作用域)

// 异步执行 依次输出0-9 (利用闭包改造)
for (var i = 0; i < 10; i++) {
  (function(j) {
    setTimeout(() => {
      // console执行时 j取的是当前作用域中的值 所以输出0-9
      console.log(j);
    }, 0);
  })(i);
}

// 还可以这样
for (var i = 0; i < 10; i++) {
  (function() {
    var j = i;
    setTimeout(() => {
      console.log(j);
    }, 0);
  })();
}

这里的原因和上面的 let/const 相同,唯一的区别是,这里的变量 i 来自闭包函数创建的作用域

for 循环中 let、var 问题

2 闭包

什么是闭包

过去我一直这样理解闭包:函数中返回一个内部函数,且内部函数使用了函数中的变量,所以判断是不是闭包,首先就找 return,然后看变量的引用情况。

翻了下你不知道的 js,发现这么理解格局有点小了,这个形式只是一种经典案例

// 经典重现
function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); // 2 ———— 朋友,这就是经典闭包。

这些也是闭包

只要函数在定义时的词法作用域以外的地方被调用,并且可以继续访问定义时的词法作用域。

回想下上一节学习的作用域,概念中的词法作用域怎么理解呢?

function outFoo() {
  var a = 2;

  function innerFun() {
    console.log(a); // 2
  }

  outBar(innerFun);
}

function outBar(fn) {
  // 此时fn就是innerFun 定义是的作用域是outFoo 在outFoo外调用
  fn(); // 朋友,这也是闭包!
}
var fn;

function outFoo() {
  var a = 2;

  function innerFun() {
    console.log(a);
  }

  fn = innerFun; // 将innerFun分配给全局变量
}

function outBar() {
  // 此时fn还是innerFun 定义是的作用域是outFoo 在outFoo外调用
  fn(); // 朋友,这还是闭包!
}

outFoo();

outBar(); // 2

IIFE 立即执行函数是不是闭包

立即执行函数是闭包,其实这个说法是有争议的,仁者见仁,智者见智吧

我比较认可你不知道的js作者的观点是,“IIFE 本身 并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包”

var a = 2;

(function IIFE() {
  console.log(a);
})();

闭包的用处

封装模块

var foo = (function CoolModule(id) {
  function change() {
    // 修改公共API
    publicAPI.identify = identify2;
  }

  function identify1() {
    console.log(id);
  }

  function identify2() {
    console.log(id.toUpperCase());
  }

  var publicAPI = {
    change: change,
    identify: identify1,
  };

  return publicAPI;
})('foo module');

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODUL

业务场景

img 上报请求丢失问题,img 是 report 函数中的局部变量,当 report 函数的

调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。

var report = function(src) {
  var img = new Image();
  img.src = src;
};
report('http://xxx.com/getUserInfo');

// 利用闭包,report保存了innerFun的引用,innerFun又引用了imgs,
   所以imgs不会释放,除非指定report = null; 
   释放innerFun的引用,才会在下一次垃圾回收中清除
// js中垃圾回收使用了引用计数的机制,只有变量没有被引用,才会被回收

var report = (function() {
  var imgs = [];
  return function innerFun(src) {
    var img = new Image();
    imgs.push(img);
    img.src = src;
  };
})();

模块依赖加载器

书中的这个加载器的例子还是挺有意思的,看上去有 SeaJS/RequesJS 的感觉,挺巧妙的。

var MyModules = (function Manager() {
  var modules = {};

  function define(name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply(impl, deps);
  }

  function get(name) {
    return modules[name];
  }

  return {
    define: define,
    get: get,
  };
})();
MyModules.define('bar', [], function() {
  function hello(who) {
    return 'Let me introduce: ' + who;
  }

  return {
    hello: hello,
  };
});

MyModules.define('foo', ['bar'], function(bar) {
  var hungry = 'hippo';

  function awesome() {
    console.log(bar.hello(hungry).toUpperCase());
  }

  return {
    awesome: awesome,
  };
});

var bar = MyModules.get('bar');
var foo = MyModules.get('foo');

console.log(bar.hello('hippo')); // Let me introduce: hippo

foo.awesome(); // LET ME INTRODUCE: HIPPO

mdn 闭包

闭包什么时候销毁

js 闭包的理解、运用和销毁

第 3 章 3.3 高阶函数

所谓高阶函数,是指使用其他函数作为参数、或者返回一个函数作为结果的函数

函数作为参数

常用的数组方法,比如 map/reduce/filter/sort 等。

面向切面编程(AOP) 解决了什么问题

使用 AOP 之前

有这样一个场景,第一天,产品说:现在需要在订单提交前后,增加统计时间的功能,有没有难度?当然没有,几行代码,轻松搞定

let submitOrder = () => {
  let nowTime = +new Date();
  console.log('提交订单 开始计时', nowTime);
  /*提交订单 原逻辑*/
  let spendTime = +new Date() - nowTime;
  console.log('提交订单 耗时计算', spendTime);
};

第二天,产品说:订单提交前后,再加个埋点吧,有没有问题?当然没有,再加几行。。

随着不确定需求增加,代码越来越长。长倒还好,关键部分逻辑揉在一块,剪不断理还乱~

我们可能都没注意,函数的名字是 submitOrder,但是这也太不专一了。这就是传说中的海王函数吧~

本以为游进了哥哥的鱼塘,没想到哥哥是个海王 ,哥哥真心不挑

let submitOrder = () => {
  const nowTime = +new Date();
  // 继续增加提交前库存校验
  // 继续增加...
  console.log('提交订单之前 数据上报');
  console.log('提交订单 原逻辑');
  // 继续增加失败统计
  // 继续增加...
  const spendTime = +new Date() - nowTime;
  console.log(spendTime);
  console.log('提交订单之后 再次数据上报');
};

AOP 登场

还是上面这个需求,我们是用 AOP 的思想实现一下,先请出主角,2 个切面函数

在原型链上增加或修改方法,需要谨慎。因为是全局的,有可能会冲突,而且问题不好排查。

Function.prototype.before = function(beforefn) {
  var self = this; // 保存原函数的引用
  return function() {
    // 返回包含了原函数和新函数的"代理"函数 function拥有before/after方法
    beforefn.apply(this, arguments); // 执行新函数,修正 this
    return self.apply(this, arguments); // 执行原函数
  };
};
Function.prototype.after = function(afterfn) {
  var self = this;
  return function() {
    // 执行原函数
    var ret = self.apply(this, arguments);
    // 执行after
    afterfn.apply(this, arguments);
    // 返回原函数的返回值
    return ret;
  };
};

修改后的效果

let submitOrder = () => {
  console.log('提交订单 原逻辑');
};

// 时间统计函数
const timeLog = fn => {
  let nowTime;
  return fn
    .before(() => {
      nowTime = +new Date();
      console.log('提交订单 开始计时', nowTime);
    })
    .after(() => {
      const spendTime = +new Date() - nowTime;
      console.log('提交订单 耗时计算', spendTime);
      return this;
    });
};

const reportLog = fn => {
  return fn
    .before(() => {
      console.log('提交订单之前 数据上报');
    })
    .after(() => {
      console.log('提交订单之后 再次数据上报');
    });
};

const submitOrderAddTimeLog = timeLog(submitOrder);
const submitOrderAddTimeLogAndReportLog = reportLog(submitOrderAddTimeLog);
submitOrderAddTimeLogAndReportLog();

// 提交订单之前 数据上报
// 提交订单 开始计时 1618496989387
// 提交订单 原逻辑
// 提交订单 耗时计算 2
// 提交订单之后 再次数据上报

这样就实现了,不修改原函数逻辑的同时,扩展一些未来的功能。函数名字是取的长了些,但是注释都省得写了~

高阶函数应用

柯里化与反柯里化

柯里化

柯里化就是把接受多个参数的函数,变换成接受单参数的函数,内部再返回调用下一个单参数的函数。书中实现了通用的 currying 函数,这个有点东西,我们来分析一下

/**
 * 这里实现了一个通用的包装函数,保存args的引用
 * 调用时传递参数 则push进args,同时返回当前函数(也就是说可以连续调用)
 * 调用时不传参数 则使用args执行传入的函数fn
 */

var currying = function(fn) {
  var args = [];
  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args);
    } else {
      [].push.apply(args, arguments);
      return arguments.callee;
    }
  };
};

// 这里使用自执行函数只是为了模仿块级作用域 方便理解完全可以改为普通函数
var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0, l = arguments.length; i < l; i++) {
      money += arguments[i];
    }
    return money;
  };
})();

var cost = currying(cost); // 转化成 currying 函数
cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值
// cost(100)(200)(300) 还可以这样调用
console.log(cost()); // 求值并输出:600

反柯里化

反柯里化如果按照字面意思,就是把接受单个参数的函数,变换成接受多参数的函数。

这样理解也没问题,但是总感觉哪里不对,另外一种更通俗的解释是函数的借用,是函数能够接受处理其他对象,通过借用泛化、扩大了函数的使用范围。

Function.prototype.uncurrying = function() {
  // self 是调用uncurrying的函数 也即是Array.prototype.push
  var self = this;
  return function() {
    /**
     * arguments是push传入的参数 [{"length": 1,"0": 1}, 2] (类数组)
     * obj是从arguments截取的第一个参数 {"length": 1,"0": 1}
     * 此时arguments为[2](类数组)
     */

    var obj = Array.prototype.shift.call(arguments);
    // Array.prototype.push.apply(obj, [2])
    return self.apply(obj, arguments);

    // 分析下书上的另外一种实现 有种解方程的感觉
    // return Function.prototype.call.apply(self, arguments);
    /**
     * Function.prototype.call.apply(self, arguments)
     * -> (Function.prototype.call).apply(self, arguments)
     * -> (Function.prototype.call).call(self, obj, 2)
     * -> self.call(obj, 2)
     * -> Array.prototype.push.apply(obj, [2])
     */
  };
};

var push = Array.prototype.push.uncurrying();
var obj = {
  length: 1,
  '0': 1,
};
// push从接收1个参数 变为接收2个参数
push(obj, 2);
console.log(obj); // 输出:{0: 1, 1: 2, length: 2}

// 如果call.apply让你无语的话,下面这个就有点崩溃了
var uncurrying = Function.prototype.uncurrying.uncurrying();
var pushAgain = uncurrying(Array.prototype.push);
pushAgain(obj, 2);
console.log(obj); // 输出:{0: 1, 1: 2, length: 2}

你没有看错,uncurrying 还能借用自己,这也许就是人们说 js 灵活的原因吧。

因为 Function.prototype.uncurrying 是函数,我们在函数的原型链上增加了 uncurrying
方法,试试下面这个你就知道为什么了

console.log(Function.prototype.uncurrying.uncurrying.uncurrying)

原型链就像黑洞,没有尽头

函数节流 throttle

原理:使用 setTimeout 延迟执行,如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求

var throttle = function(fn, interval) {
  var __self = fn, // 保存需要被延迟执行的函数引用
    timer, // 定时器
    firstTime = true; // 是否是第一次调用
  return function() {
    var args = arguments,
      __me = this;
    if (firstTime) {
      // 如果是第一次调用,不需延迟执行
      __self.apply(__me, args);
      return (firstTime = false);
    }
    if (timer) {
      // 如果定时器还在,说明前一次延迟执行还没有完成
      return false;
    }
    timer = setTimeout(function() {
      // 延迟一段时间执行
      clearTimeout(timer);
      timer = null;
      __self.apply(__me, args);
    }, interval || 500);
  };
};

分时函数

书上的例子是在短时间内往页面中大量添加 DOM 节点,优化为每隔一段时间创建一些节点。其实仔细想下,我们平时的项目也是这样,只不过这个事情是框架做的。

react16 采用了时间片的方式解决卡顿问题,只不过它分的更细,以帧为单位,所以看上去渲染很流畅。

模拟 react 实现分片渲染,这样就不用设置时间间隔,浏览器不忙的时候自动执行

var timeChunk = function(ary, fn, count) {
  var obj, t;
  var len = ary.length;
  var start = function() {
    for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
      var obj = ary.shift();
      fn(obj);
    }
  };
  return function(deadline) {
    while (deadline.timeRemaining() > 0 || deadline.didTimeout) {
      start();
    }
    if (ary.length) {
      requestIdleCallback(arguments.callee);
    }
  };
};

var ary = [];
for (var i = 1; i <= 1000; i++) {
  ary.push(i);
}
var renderFriendList = timeChunk(
  ary,
  function(n) {
    var div = document.createElement('div');
    div.innerHTML = n;
    document.body.appendChild(div);
  },
  8,
);

requestIdleCallback(renderFriendList, { timeout: 2000 });

惰性加载

核心是函数重写,第一次执行后原函数就被替换了

var addEvent = function(elem, type, handler) {
  if (window.addEventListener) {
    addEvent = function(elem, type, handler) {
      elem.addEventListener(type, handler, false);
    };
  } else if (window.attachEvent) {
    addEvent = function(elem, type, handler) {
      elem.attachEvent('on' + type, handler);
    };
  }
  // 第一次执行 重写addEvent = newFunction 调用newFunction,后续直接执行newFunction
  addEvent(elem, type, handler);
};

JS 中的反柯里化

用 AOP 改善 javascript 代码

你可能感兴趣的:(js,前端小技巧,study,js,javascript)