Skip to content
On this page

JS基础

闭包与作用域链

  • 闭包(闭包函数)的概念:指有权访问另一个函数作用域中的变量的函数,一般情况就是在一个函数中包含另一个函数。

  • 闭包的实现原理:其实是利用了作用域链的特性,我们都知道作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条。当内部函数被返回或者赋值给其他变量时,它会保持对外部函数作用域中变量的引用,这样就形成了闭包。

  • 闭包的作用:主要有以下几个方面:

    • 访问函数内部变量:通过闭包可以访问到外部函数中定义的局部变量或参数,这些变量对于其他函数来说是不可见的。
    • 保持函数在环境中一直存在,不会被垃圾回收机制处理:通过闭包可以使得外部函数在执行完毕后仍然存在于内存中,因为有内部函数引用它。这样可以避免重复创建和销毁对象,提高性能。
    • 实现私有属性和方法:通过闭包可以实现类似于面向对象编程中私有属性和方法的功能,即只能通过特定的接口访问或修改某些数据。
  • 示例

    js
    function outer() {
      var x = 10; // 外部函数的局部变量
      function inner() {
        console.log(x); // 内部函数可以访问外部函数的局部变量
      }
      return inner; // 外部函数返回内部函数
    }
    
    var fn = outer(); // 调用外部函数,得到内部函数
    fn(); // 调用内部函数,输出10
  • 闭包的缺点主要有以下两个:

    • 闭包会使得函数中的变量都被保存在内存中,增大内存消耗,可能导致内存泄漏
    • 闭包会影响处理速度,因为每次访问一个变量时,都需要从作用域链中查找
  • 如何避免闭包导致的内存泄露

    • 避免闭包造成内存泄漏的一个常用方法是,在退出函数之前,将不使用的局部变量赋值为null。这样可以断开闭包对外部变量的引用,让垃圾回收机制回收内存。

    • 这里有一个代码示例,说明了闭包如何造成内存泄漏:

      javascript
      $(document).ready(function () {
        var button = document.getElementById('button-1');
        button.onclick = function () {
          console.log('hello');
          return false;
        };
      });

      当指定单击事件处理程序时,就创建了一个在其封闭的环境中包含button变量的闭包。而且,现在的button也包含一个对闭包的引用。这样就形成了一个循环引用,导致内存无法被回收。

  • 闭包有什么常见的应用场景

    • 闭包的应用场景有很多,这里我简单介绍几个常见的:

      • 使用闭包代替全局变量,隐藏变量,避免全局污染。
      • 函数外或在其他函数中访问某一函数内部的参数,实现数据的私有化。
      • 在函数执行之前为要执行的函数提供具体参数,实现柯里化或偏应用。
      • 在函数执行之前为函数提供只有在函数执行或引用时才能知道的具体参数,实现延迟计算或惰性加载。
      • 在循环中创建闭包,防止取到意外的值。

      什么时候要使用闭包呢?一般来说,当你需要间接访问一个变量,或者需要保持一个变量在内存中不被销毁时,就可以考虑使用闭包。但是使用闭包也要注意可能造成的内存泄漏问题,所以不要滥用闭包。

    • 示例

      js
      var data = [];
      for (var i = 0; i < 3; i++) {
        data[i] = function () {
          console.log(i);
        };
      }
      // data[0]();  // 3
      // data[1]();  // 3
      // data[2]();  // 3
      // var定义全局变量,上述代码执行过程中会将三个函数存储进data数组
      // 在调用时三个函数先在函数内部找变量i,没找到就会沿着作用域链找到全局定义的i,此时的i经过循环后已经变成了3
      
      // 闭包打印输出1,2,3
      function outter() {
        let num = 0
        return function () {
          console.log(++num)
        }
      }
      let a = outter()
      a() // 1
      a() // 2
      let b = outter()
      b() // 1
      b() // 2
      b() // 3

防抖和节流

是什么

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debouncethrottle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,15秒后准时运送一次,这是节流

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

区别

  • 防抖是将多次执行变为最后一次执行,节流是将多次执行变为在规定时间内只执行一次
  • 防抖适合用于输入框搜索提示、短信验证码、提交表单等场景,节流适合用于滚动条滚动事件、鼠标移动事件、播放进度条等场景。
  • 防抖的效果是:用户在不断输入内容时,不会发送请求,只有当用户停止输入时,才会发送请求。
  • 节流的效果是:用户在不断输入内容时,每隔一段时间就会发送请求,但不会频繁发送请求。

应用场景

  • 防抖

    • 用于输入框的搜索提示,避免用户每输入一个字母就发送请求。
    • 用于窗口的大小改变事件,避免用户每调整一次窗口就触发回调。
    • 用于按钮点击事件, 例如防止用户多次点击button重复发送登录请求
  • 节流

    • 用于滚动条的滚动事件,避免用户每滚动一像素就触发回调。
    • 用于鼠标的移动事件,避免用户每移动一次鼠标就触发回调。

代码示例

js
// 防抖:输入框搜索提示
let input = document.getElementById("input");
let timer = null;
input.addEventListener("input", function() {
  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    // 发送请求
    console.log(input.value);
  }, 500);
});

// 节流:滚动条滚动事件
let scroll = document.getElementById("scroll");
let lastTime = 0;
scroll.addEventListener("scroll", function() {
  let nowTime = Date.now();
  if (nowTime - lastTime > 1000) {
    // 执行回调
    console.log(scroll.scrollTop);
    lastTime = nowTime;
  }
});

数组扁平化

使用递归函数,遍历数组中的每个元素,如果是数组就继续递归调用,否则就将元素添加到一个新的数组中

js
var newArr = []; // 用于存放转换后的数组
function arrOfOneDimension(arr) {
  for (let key of arr) { // for-of遍历可迭代对象
    if (Array.isArray(key)) {
      arrOfOneDimension(key);//如果还是数组继续递归调用
    } else {
      newArr.push(key);
    }
  }
  return newArr;
}

也可以用数组自带的flat方法实现:

js
let arr = [1, [2, 3, [4, 5], '7'], 'a,b]cd'];
arr.flat(Infinity); // 返回 [1, 2, 3, 4, 5, '7', 'a,b]cd']

多维数组去重

JS如何实现多维数组去重,有多种方法,这里给你一个简单的例子,使用递归和indexOf方法

javascript
// 定义一个函数,用来判断一个元素是否在一个数组中
function contains(arr, item) {
  for (let i = 0; i < arr.length; i++) {
    // 如果是一维数组,直接比较
    if (arr[i] === item) {
      return true;
    }
    // 如果是多维数组,递归调用
    if (Array.isArray(arr[i]) && contains(arr[i], item)) {
      return true;
    }
  }
  return false;
}

// 定义一个函数,用来实现多维数组去重
function unique(arr) {
  let result = []; // 存放结果的数组
  for (let i = 0; i < arr.length; i++) {
    // 如果当前元素不在结果数组中,就添加到结果数组中
    if (!contains(result, arr[i])) {
      result.push(arr[i]);
    }
  }
  return result;
}

// 测试代码
let arr = [[1, 2, 3], [2, 3, 4], [2, 1, 3], [5, 6, 7]];
console.log(unique(arr)); // [[1,2,3],[2,3,4],[5,6,7]]

手写promise.all和promise.race

如何用js代码实现promise.all和promise.race。这两个方法都是用来将多个promise对象包装成一个新的promise对象,但是有不同的行为。

all

Promise.all()方法接收一个可迭代类型(Array,Map,Set)作为参数

它会等待所有的promise对象都成功(resolved)后才返回一个成功的promise对象,返回值是所有成功结果的数组。例如:

js
// 假设有三个异步操作
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p1');
  }, 1000);
});

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2');
  }, 2000);
});

let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3');
  }, 3000);
});

// 使用Promise.all()方法
Promise.all([p1, p2, p3]).then((result) => {
  console.log(result); // 等到p3结束后执行 [ 'p1', 'p2', 'p3' ]
}).catch((error) => {
  console.log(error); // 不会执行, 因为没有错误
});

如果有任何一个promise对象失败(rejected),它会立即返回一个失败的promise对象,并抛出错误。例如:

javascript
// 假设有三个异步操作
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p1');
  }, 1000);
});

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2');
  }, 2000);
});

let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p3');
  }, 3000);
});

// 使用Promise.all()方法
Promise.all([p1, p2, p3]).then((result) => {
  console.log(result); // 不会执行,因为p3失败了
}).catch((error) => {
  console.log(error); // 输出 'p3'
});

race

Promise.race()方法也接收一个可迭代类型作为参数

它会返回一个新的promise对象,但是只要其中任何一个promise对象率先改变状态(无论成功resolve还是失败reject),它就会跟着改变状态,并返回那个率先改变状态的结果, 这就是所谓race的含义。例如:

javascript
// 使用同样的三个异步操作

// 使用Promise.race()方法
Promise.race([p1, p2, p3]).then((result) => {
  console.log(result); // 输出 'p1',因为p1最快成功了
}).catch((error) => {
  console.log(error); // 不会执行,因为没有失败在前面
});

手撕promise.all

简单来说,要实现类似promise.all的方法,需要做以下几件事¹²:

  • 接收一个可迭代类型作为参数,并返回一个新的promise对象
  • 定义一个空数组用于存放每个promise对象的结果
  • 遍历参数中的每个元素,如果是promise对象就调用它们的then方法,并将结果添加到数组中;如果不是就直接添加到数组中
  • 如果所有元素都成功(resolved),就调用新promise对象的resolve方法,并传入结果数组;如果有任何元素失败(rejected),就调用新promise对象的reject方法,并传入错误信息

例如:

javascript
// 实现类似Promise.all()方法
function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    // 参数必须是可迭代类型
    if (!iterable || typeof iterable[Symbol.iterator] !== 'function') {
      throw new TypeError(`${iterable} is not iterable`);
    }
    // 定义一个空数组用于存放结果
    let result = [];
    // 定义一个计数器用于记录成功次数
    let count = 0;
    // 遍历参数中的每个元素
    for (let i = 0; i < iterable.length; i++) {
      // 如果是promise对象就调用then方法
      if (iterable[i] instanceof Promise) {
        iterable[i].then((value) => {
          // 将结果添加到数组中,并保持顺序
          result[i] = value;
          // 计数器加一
          count++;
          // 如果所有元素都成功,就返回成功状态和结果数组
          if (count === iterable.length) {
            resolve(result);
          }
        }, (reason) => {
          // 如果有任何元素失败,就返回失败状态和错误信息
          reject(reason);
        });
      } else {
        // 如果不是promise对象就直接添加到数组中,并保持顺序
        result[i] = iterable[i];
        // 计数器加一
        count++;
        // 如果所有元素都成功,就返回成功状态和结果数组
        if (count === iterable.length) {
          resolve(result);
        }
      }
    }
  });
}

手撕promise.race

简单来说,要实现类似promise.race的功能,需要做以下几件事¹²:

  • 接收一个可迭代类型作为参数,并返回一个新的promise对象
  • 遍历参数中的每个元素,如果是promise对象就调用它们的then方法;如果不是就直接将其包装成一个立即成功(resolved)或失败(rejected) 的 promise 对象。
  • 只要其中任何一个元素率先改变状态(无论成功还是失败),就调用新 promise 对象相应 的 resolve 或 reject 方法,并传入那个率先改变状态 的 结果

例如:

javascript
// 实现类似Promise.race()功能
function promiseRace(iterable) {
  return new Promise((resolve, reject) => {
    // 参数必须是可迭代类型且不为空, 使用Symbol.iterator迭代器属性来判断
    if (!iterable || typeof iterable[Symbol.iterator] !== 'function') {
      throw new TypeError(`${iterable} is not iterable`);
    }
    // 遍历参数中的每个元素
    for (let item of iterable) {
      // 如果是 promise 对象 就 调 用 then 方法
      if (item instanceof Promise) {
        item.then(resolve, reject);
      } else {
        // 如果 不 是 promise 对象 就 直接将其包装成一个立即成功或失败 的 promise 对象
        resolve(item);
      }
    }
  });
}

上次更新于: