Skip to content

02 - JS 进阶

目录


一、作用域与闭包

作用域的分类

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6,由 let/const 创建)

在当前作用域中没有定义的变量称为自由变量,访问它时会不断向定义时的上级作用域寻找(词法作用域)。

闭包

定义:自由变量的查找,是在函数定义的地方向上级作用域寻找,与函数执行的地方无关。

两种典型形式

  1. 函数作为参数被传递,在另一个地方执行:

    js
    function print(fn) {
      const a = 200;
      fn();
    }
    const a = 100;
    function fn() {
      console.log(a); // 打印 100(在定义的地方找 a)
    }
    print(fn);
  2. 函数作为返回值:

    js
    function create() {
      const a = 100;
      return function() {
        console.log(a); // 打印 100
      };
    }
    const fn = create();
    const a = 200;
    fn(); // 100

闭包的实际应用:隐藏数据

js
// 利用闭包实现私有缓存工具
function createCache() {
  const data = {}; // 不对外暴露
  return {
    set(key, val) { data[key] = val; },
    get(key) { return data[key]; }
  };
}
const c = createCache();
c.set('a', 100);
console.log(c.get('a')); // 100

闭包隐藏数据示意

经典闭包面试题:点击事件中的 i

js
// ❌ 写法一:var 声明,闭包共享变量
let i, a;
for (i = 0; i < 10; i++) {
  a = document.createElement('a');
  a.innerHTML = i + '<br/>';
  a.addEventListener('click', function(e) {
    e.preventDefault();
    alert(i); // 点击后均弹出 10(闭包捕获的是同一个 i)
  });
  document.body.appendChild(a);
}

// ✅ 写法二:let 声明,每次循环有独立的块级作用域
for (let i = 0; i < 10; i++) {
  a = document.createElement('a');
  a.innerHTML = i + '<br/>';
  a.addEventListener('click', function(e) {
    e.preventDefault();
    alert(i); // 点击后弹出对应的 0~9
  });
  document.body.appendChild(a);
}

二、this 指向

this 取什么值,是在函数执行时确认的,不是定义时确认的。

五种情况

调用方式this 指向
普通函数直接调用window(非严格模式) / undefined(严格模式)
对象方法调用调用该方法的对象
new 构造函数新创建的实例对象
call/apply/bind指定的第一个参数
箭头函数继承外层(定义时所在)作用域的 this

普通函数与对象中的 this

js
let obj = {
  id: 1,
  myName() {
    console.log(this); // { id: 1, myName: f }(obj 调用,this 是 obj)
  },
  wait() {
    setTimeout(function() {
      console.log(this); // window(setTimeout 里普通函数由浏览器调用)
    });
  },
  waitAgain() {
    setTimeout(() => {
      console.log(this); // obj(箭头函数继承外层 this)
    });
  }
};

箭头函数中的 this

js
var name = 'name 1';
const stu = {
  name: 'name 2',
  sayName1: () => {
    console.log(this.name); // name 1(外层是全局,this 是 window)
  },
  sayName2() {
    console.log(this.name); // name 2(obj 调用)
  },
  sayName3: function() {
    return () => {
      console.log(this.name); // name 2(箭头函数继承外层函数的 this)
    };
  },
  sayName4: () => {
    return function() {
      console.log(this.name); // name 1(普通函数由调用者决定)
    };
  }
};

this指向各场景this指向总结

手写 bind

js
Function.prototype.myBind = function(context, ...args) {
  const self = this;
  const bindedFunc = function(...callArgs) {
    // 如果通过 new 调用,this 指向新实例;否则指向 context
    const newThis = (this instanceof bindedFunc) ? this : context;
    return self.apply(newThis, args.concat(callArgs));
  };
  // 保留原型链
  if (self.prototype) {
    bindedFunc.prototype = Object.create(self.prototype);
  }
  return bindedFunc;
};

手写bind示意


三、箭头函数

与普通函数的区别

特性普通函数箭头函数
this 绑定由调用方式决定继承外层作用域的 this
arguments无(用 ...rest 代替)
new 构造支持❌ 不支持(不能作为构造函数)
yield支持❌ 不支持(不能作为 Generator)
call/apply/bind 改变 this支持❌ 无效

什么时候不能用箭头函数

  1. ❌ 作为对象的方法(this 会指向外层,而非对象)
  2. ❌ 作为构造函数(new 会报错)
  3. ❌ 需要使用 arguments(箭头函数没有)
  4. ❌ 事件监听中需要 this 指向事件源时
  5. Vue 2 Options API / Vue 生命周期函数(Vue 组件本质是 JS 对象)

Vue 3 Composition API 中可以完全使用箭头函数。 React class 组件(本质是 ES6 class)中可以使用箭头函数定义方法。


四、Generator 生成器

js
// 定义生成器函数:function* 声明
function* myGenerator() {
  yield 1;  // 暂停,返回 1
  yield 2;  // 暂停,返回 2
  return 3; // 结束,done: true
}

const gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: true }

双向通信(yield 传值)

js
function* counter() {
  const a = yield 'first';
  console.log('接收到:', a);
  const b = yield 'second';
  console.log('接收到:', b);
  return 'done';
}
const g = counter();
g.next();       // { value: 'first', done: false }
g.next(100);    // 输出 '接收到: 100',{ value: 'second', done: false }
g.next(200);    // 输出 '接收到: 200',{ value: 'done', done: true }

五、for...in 与 for...of

特性for...infor...of
遍历内容key(键)value(值)
遍历对象✅ 支持❌ 不支持(对象不是可迭代的)
遍历数组✅(得到索引)✅(得到值)
遍历 Map/Set
遍历 Generator
本质可枚举(enumerable)可迭代(iterable,有 Symbol.iterator
js
// for...in 遍历对象
const obj = { a: 1, b: 2 };
for (let key in obj) {
  console.log(key); // 'a', 'b'
}

// for...of 遍历数组
const arr = [10, 20, 30];
for (let val of arr) {
  console.log(val); // 10, 20, 30
}

表面上是 for...in vs for...of,本质上是可枚举可迭代的区别。


六、函数柯里化

将接受多个参数的函数,转化为接受单个参数的一系列函数的技术。

js
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return (...args2) => curried.apply(this, args.concat(args2));
    }
  };
}

// 使用示例
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6

七、防抖与节流

防抖(Debounce)

定义:连续触发时,只有停止操作后等待指定时间才执行。适合搜索框输入。

js
function debounce(fn, delay = 200) {
  let timer = 0;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = 0;
    }, delay);
  };
}

// 示例
const input = document.getElementById('input1');
input.addEventListener('keyup', debounce(() => {
  console.log('搜索:' + input.value);
}, 300));

闭包作用timer 变量不会被销毁,始终在 debounce 作用域中,确保可以清除上一次的 setTimeout

节流(Throttle)

定义:高频触发时,按固定时间间隔执行。适合 scroll、drag 等高频事件。

js
function throttle(fn, delay = 100) {
  let timer = 0;
  return function(...args) {
    if (timer) return;
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = 0;
    }, delay);
  };
}

// 示例
const card = document.getElementById('card1');
card.addEventListener('drag', throttle((e) => {
  console.log(`鼠标位置: ${e.offsetX}, ${e.offsetY}`);
}, 300));

对比

防抖(Debounce)节流(Throttle)
触发时机停止操作后延迟触发(只触发最后一次)按固定频率触发(从高频中择几次)
适用场景搜索框输入、表单提交scroll、drag、resize 事件

实际工作中可使用 lodash_.debounce_.throttle


八、V8 内存管理与垃圾回收

分代垃圾回收策略

新生代(Young Generation)

  • 存储新创建的对象(大多数对象短命)
  • 使用 Scavenge(复制算法)
    • 内存分为 from space 和 to space 两半
    • GC 时将存活对象复制到 to space,其余丢弃
    • 角色交换,暂停时间短(Minor GC)
  • 经历多次 GC 仍存活的对象晋升到老生代

老生代(Old Generation)

  • 存储长生命周期的对象
  • 使用 标记-清除(Mark-Sweep) + 标记-整理(Mark-Compact)
    • 标记所有可达对象
    • 清除未标记对象
    • 整理内存碎片(移动存活对象到一端)

垃圾回收触发条件

  • 内存阈值:堆内存达到一定比例时触发
  • 分配失败:新对象无法在当前空间分配时触发
  • 隐式触发:事件循环空闲时自动运行

<< 返回首页