闭包

  • 闭包:是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
  • 示例
    function createFunction() {
      var result = new Array()
      for (var i = 0; i < 10; i++) {
        result[i] = function() {
          return i;
        }
      }
      return result;
    } // 每个函数返回都是 10
    //添加闭包
    function createFunction() {
      var result = new Array()
      for (var i = 0; i < 10; i++) {
        result[i] = function(num) {
          return function() {
            return num
          }
        }(i)
      }
      return result
    }

this对象

  • 在全局函数中,this等于window(非严格模式), 或者undefined(严格模式)
  • 当函数作为某个对象的方法调用时,this等于那个对象,不过,匿名函数的执行环境具有全局性,因此其this对象通过指向window。
  • 作为构造函数调用,this 指代new 的实例对象
  • 通过apply() 或 call()改变函数执行环境的情况下,this就会指向其他对象。
  • Node模块或ES6模块中,this返回的是当前模块
  • 示例
  var name = "The Window"
  var object = {
    name: "My Object",
    getNameFunc: function() {
      return function() {
        return this.name
      }
    }
  }
  console.log(object.getNameFunc()())
  // "The Window"(非严格模式)  undefined(严格模式)
  /*匿名函数的执行环境具有全局性,因此其this通过指向window*/
  var name = "The Winodw"
  var object = {
    name: "My Object",
    getName: function() {
      return this.name;
    }
  }
  object.getName() // "My Object"
  (object.getName)() //"My Object"
  (object.getName = object.getName)() // "The Window",在非严格格式下

闭包的作用

  • 使用闭包可以在JavaScript中模仿块级作用域,要点如下:

    • 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
    • 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。
  • 闭包还可以用于在对象中创建私有变量,相关概念和要点如下:

    • 即使JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。
    • 有权访问私有变量的公有方法叫做特权方法。
    • 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

模仿块级作用域

  • javascript没有块级作用域的概念,这意味着在块级语句中定义的变量,实际上是在包含函数中而非语句中创建的。匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(通常称为私有作用域)的匿名函数的语法如下:
(function(){
     // 这里是块级作用域
})()
  • 以一个例子说明
    例1:
    function outputNumber() {
      for (var i = 0; i < 10; i += 1) {}
      console.log(i);
    }
    outputNumber();
    // 此时会输出 10 

例2:

    function outputNumber() {
      (function () {
        for (var i = 0; i < 10; i += 1) {
          console.log('i', i);
        }
      })();
      console.log(i);
    }
    outputNumber();
添加一个匿名立即执行函数,此时就会报错 Uncaught ReferenceError: i is not defined, 达到了块级作用域的效果

私有变量

  • 严格来讲,javascript中没有私有成员的概念,所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
  • 如果在这个函数内部创建一个闭包,那么闭包通过t自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于说私有变量的公有方法。
  • 我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。

    1. 在构造函数中定义特权方法

  • 示例
    function myObject() {
      // 私有变量和私有函数
      var privateVariable = 10;
      function privateFunction() {
        return false;
      }
      // 特权方法
      this.publicMethod = function () {
        privateVariable++;
        return privateFunction();
      };
    }
  • 在构造函数中定义特权方法的缺点:必须使用构造函数模式来达到目的,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题

    2. 静态私有变量

  • 通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
  • 示例
    (function () {
      // 私有变量和私有函数
      var privateVariable = 10;
      function privateFunction() {
        return false;
      }
      // 构造函数
      MyObject = function () {};
      // 特权方法
      MyObject.prototype.publicMethod = function () {
        privateVariable++;
        return privateFunction();
      };
    })();
  • 这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

    模块模式

  • 模块模式则是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。javascript是以对象字面量的方式来创建单例对象的。
  • 示例
    var singleton = {
      name:value,
      method: function(){
        // do something
      }
    }
  • 模块模式通过为单例添加私有变量和特权方法能够使其得到增强
    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());

      var app = new BaseComponent();
      app.getComponentCount = function () {
        return components.length;
      };
      app.registerComponent = function (component) {
        if (typeof component === 'object') {
          components.push(component);
        }
      };

      return app;
    };
// 限制application必须是BaseComponent的实例

过时的闭包

  • 问题示例
    function createIncrement(i) {
      let value = 0;
      function increment() {
        value += i;
        console.log(value);
        const message = `Current value is ${value}`;
        return function logValue() {
          console.log(message);
        };
      }

      return increment;
    }

    const inc = createIncrement(1);
    const log = inc(); // 打印 1
    inc(); // 打印 2
    inc(); // 打印 3
    // 无法正确工作
    log(); // 打印 "Current value is 1"

修复过时闭包:

  1. 解决过时闭包的第一种方法是找到捕获最新变量的闭包。就是从最后一次调用 inc() 返回的闭包。
const inc = createIncrement(1);

inc();  // 打印 1
inc();  // 打印 2
const latestLog = inc(); // 打印 3
// 正常工作
latestLog(); // 打印 "Current value is 3"

2.第二种方法是让logValue()直接使用 value

function createIncrement(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
      const message = `Current value is ${value}`;
      console.log(message);
    };
  }

  return increment;
}

const inc = createIncrement(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
// 正常工作
log();             // 打印 "Current value is 3"

React Hook

useEffect中过时闭包的问题及解决

  • 示例
 import React, { useEffect, useState } from 'react';

const HomePage = () => {
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  let tm;
  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    setInterval(() => {
      setCount1((prev) => {
        return prev + 1;
      });
    }, 1000);
  }, []);

  useEffect(() => {
    tm = setInterval(() => {
      setCount2(count2 + 1);
    }, 1000);
    return () => {
      clearInterval(tm);
      tm = null;
    };
  }, [count2]);

  return (
    <div>
      {/* count 一直显示为 1 */}
      <p>计数:{count}</p>
      {/* 以下两种方式正常计数 */}
      <p>计数-callback:{count1}</p>
      <p>计数-依赖项:{count2}</p>
    </div>
  );
};

防抖与节流

  • 节流:每隔一段时间触发一次事件
  // 方式1:
  const throlttle3 = (fn, delay) => {
    let flag = true;
    return function () {
      if (flag) {
        flag = false;
        setTimeout(() => {
          fn.apply(this, arguments);
          flag = true;
        }, delay);
      }
    };
  };
  // 方式2 : 时间戳
  const throlttle = (fn, delay) => {
    let prev = Date.now();
    return function () {
      const now = Date.now();
      if (now - prev >= delay) {
        fn.apply(this, arguments);
        prev = Date.now();
      }
    };
  };
 // 方式3: 定时器
  const throlttle2 = (fn, delay) => {
    let tm = null;
    return function () {
      if (!tm) {
        tm = setTimeout(() => {
          fn.apply(this, arguments);
        }, delay);
      }
    };
  };

  // 第一次立即执行
  const throlttle4 = (fn, delay) => {
    let tm = null;
    let lastTm = Date.now();
    return function () {
      let curTm = Date.now();
      var rest = delay - (curTm - lastTm);
      clearTimeout(tm);
      if (rest <= 0) {
        fn.apply(this, arguments);
        lastTm = Date.now();
      } else {
        tm = setTimeout(() => {
          fn.apply(this, arguments);
        }, rest);
      }
    };
  };
  • 防抖:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法。
  const debounce = (fn, delay) => {
    ``;
    let tm = null;
    return function () {
      if (tm) clearTimeout(tm);
      tm = setTimeout(() => {
        fn.apply(this, arguments);
      }, delay);
    };
  };

柯里化

  • 是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
  • 示例
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9

坚果面包
21 声望0 粉丝