前端面试中值得关注的js题

535 阅读18分钟

“这就像花一样。如果你爱上了一朵生长在一颗星星上的花,那么夜间,你看着天空就感到甜蜜愉快,所有的星星上都好像开着花。” -- 《小王子》(诠释:爱一个人会让你的整个世界都变得美好,充满ta的影子。)


总结了几道近期前端面试中问到的值得一看的js面试题,其实有些问题很基础,但是长时间没看又容易遗忘,希望这篇文章对你我有所帮助

1. 什么是事件循环机制?

面试极大概率会问!!!

如果我们把同步代码看作第一个宏任务,那么事件循环机制就是先执行一个宏任务,然后执行由这个宏任务运行过程中产生的所有Promise的回调等微任务。但由于微任务队列优先级高,如果执行一个微任务过程中又产生了一个微任务,那么这个新生的微任务会被放置队列尾部被执行且早于下一个宏任务。事件循环会不断重复“执行一个宏任务 → 清空微任务队列”的过程,如果涉及渲染,浏览器则会清空微任务队列后在进行布局和绘制。

特性宏任务(Macrotask/Task)微任务(Microtask)
示例setTimeout, setInterval, setImmediate (Node), I/O 操作, UI 渲染, requestAnimationFramePromise 的回调 (.then, .catch, .finally), MutationObserver, queueMicrotask
谁提供由浏览器或 Node.js 环境提供由 JavaScript 引擎自身提供(ES6 规范)
执行时机在一次事件循环中只执行一个在一次事件循环中全部执行完毕(直到队列清空)
队列优先级高(总是在当前宏任务结束后、下一个宏任务开始前执行)

“如果你驯服了我,我们就互相不可缺少了。对我来说,你就是世界上唯一的了;我对你来说,也是世界上唯一的了。”-- 《小王子》(“驯服”即建立羁绊。诠释:爱是彼此需要,彼此成为对方的唯一。)

2. 什么是闭包?

也是老生常谈的面试题!

一个内部函数引用了它外部函数中的变量,当外部函数被调用执行后,其本地的执行上下文(包括其变量对象)通常应该被销毁,但这个被引用的变量和内部函数捆绑在了一起不会被回收,从而形成了闭包。 简单来说,闭包就是能够访问其他函数内部变量的函数

它的主要应用场景可以归结为:创建私有数据,并持久化地操作这些数据

1. 创建私有变量(数据封装)

这是闭包最经典的应用。JavaScript 没有原生支持私有成员(ES6 的 # 语法除外),但通过闭包可以模拟。

function createCounter() {
  let count = 0; // count 是一个私有变量,外部无法直接访问

  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getValue: function() {
      return count;
    }
  };
}

const myCounter = createCounter();
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
console.log(myCounter.decrement()); // 1
console.log(myCounter.count); // undefined (无法直接访问)
// count 变量被安全地封装在闭包中,只能通过暴露的方法操作

2. 回调函数和事件处理

在异步操作(如定时器、事件监听、Ajax 请求)中,回调函数经常需要记住它被创建时的环境。

function setupAlert(message, delay) {
  setTimeout(function() { // 这个回调函数就是一个闭包
    alert(message); // 它记住了外部的 message 参数
  }, delay);
}

setupAlert('Hello World!', 2000); // 2秒后弹出 'Hello World!'

在循环中创建闭包是一个经典问题,也体现了闭包的作用:

// 错误做法:所有按钮都提示 “Button 6 clicked”
for (var i = 1; i <= 5; i++) {
  var btn = document.createElement('button');
  btn.textContent = 'Button ' + i;
  btn.onclick = function() {
    alert('Button ' + i + ' clicked'); // 这里的 i 是循环结束后的最终值 6
  };
  document.body.appendChild(btn);
}

// 正确做法:使用闭包或 let 创建独立的作用域
for (var i = 1; i <= 5; i++) {
  (function(j) { // 立即执行函数创建了一个新作用域,j 保存了当前 i 的值
    var btn = document.createElement('button');
    btn.textContent = 'Button ' + j;
    btn.onclick = function() {
      alert('Button ' + j + ' clicked'); // 闭包记住了当前作用域的 j
    };
    document.body.appendChild(btn);
  })(i); // 将 i 作为参数 j 传入
}

// 现代更简单的做法:使用 let(其块级作用域天然解决了这个问题)
for (let i = 1; i <= 5; i++) {
  let btn = document.createElement('button');
  btn.textContent = 'Button ' + i;
  btn.onclick = function() {
    alert('Button ' + i + ' clicked'); // 每个 i 都在一个独立的块级作用域中
  };
  document.body.appendChild(btn);
}

3. 函数柯里化(Currying)

柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数的技术。

function makeAdder(x) { // 接收第一个参数 x
  return function(y) { // 返回一个闭包,它记住了 x
    return x + y; // 闭包可以操作外部的 x 和内部的 y
  };
}

const add5 = makeAdder(5); // 创建一个新函数,它总是给参数加 5
const add10 = makeAdder(10); // 创建一个新函数,它总是给参数加 10

console.log(add5(2));  // 7 (5 + 2)
console.log(add10(2)); // 12 (10 + 2)

4. 模块化模式(Module Pattern)

在 ES6 之前,闭包是创建模块、避免全局污染的主要方式。

var MyModule = (function() {
  var privateVar = 'I am private'; // 私有变量

  function privateFunction() {
    console.log(privateVar);
  }

  return { // 返回公共接口
    publicMethod: function() {
      privateFunction(); // 公共方法可以访问私有成员
    },
    setData: function(data) {
      privateVar = data;
    }
  };
})();

MyModule.publicMethod(); // "I am private"
MyModule.setData('New data');
MyModule.publicMethod(); // "New data"
console.log(MyModule.privateVar); // undefined (无法访问)

总结

场景核心目的关键点
私有变量数据封装与隐藏通过内部函数返回一个操作私有数据的公共接口
回调/事件处理保持状态函数记住它被定义时的上下文环境(变量值)
函数柯里化部分应用,功能复用预先填充一些参数,生成一个更具体的新函数
模块化组织代码,命名空间使用 IIFE 创建独立作用域,只暴露必要的部分

简单理解,只要你在一个函数内部定义了另一个函数,并且这个内部函数访问了外部函数的变量,那么你就创建了一个闭包。它使得数据能够“存活”于函数调用之后。

“如果你说你在下午四点来,从三点钟开始,我就开始感觉很快乐,时间越临近,我就越来越感到快乐。” -- 《小王子》 (诠释:爱是期待,是即将见到所爱之人时那种按捺不住的喜悦。)

3. object \ map \ weakmap 有啥区别

这个面试题几年没看了,昨天被问到居然想不起了,特别值得好好复习!篇幅较长、值得好好看!

开门见山

  • Object:最通用的基础数据结构,键只能是字符串或 Symbol;用来处理简单的、需要序列化的、键为字符串的数据结构。
  • Map:更强大的键值对集合,键可以是任何类型,且有序;用来处理复杂的、需要频繁操作和遍历的键值对集合。
  • WeakMap:专为对象作为键设计的弱引用集合,不阻止垃圾回收;用来处理需要与对象生命周期绑定的元数据,避免内存泄漏。

多个维度进行详细对比:

特性ObjectMapWeakMap
键的类型字符串、Symbol任意值(对象、函数等)只接受对象
键的顺序无序(ES6后有一定规则)插入顺序无序(不可迭代)
大小获取需手动计算 Object.keys(obj).lengthsize 属性size 属性
迭代性需要 Object.keys() 等方法直接可迭代不可迭代
默认键有原型链(可能有意外键)纯净,无默认键纯净,无默认键
性能频繁增删时较差频繁增删时更优专为特定场景优化
垃圾回收强引用,阻止回收强引用,阻止回收弱引用,不阻止回收
序列化可用 JSON.stringify()不能直接序列化不能序列化

详细解释

1. 键的类型

  • Object:键只能是字符串Symbol。如果用其他类型作为键,会被自动转换为字符串。

    const obj = {};
    const key = { id: 1 };
    
    obj[key] = 'value'; // 键会被转换为字符串 "[object Object]"
    console.log(obj); // { "[object Object]": "value" }
    
  • Map:键可以是任何类型的值,包括对象、函数、数组等。

    const map = new Map();
    const objKey = {};
    const funcKey = function() {};
    
    map.set(objKey, '对象作为键');
    map.set(funcKey, '函数作为键');
    console.log(map.get(objKey)); // '对象作为键'
    
  • WeakMap只接受对象作为键null 除外)。

    const weakMap = new WeakMap();
    const objKey = {};
    
    weakMap.set(objKey, '有效');
    // weakMap.set('string', '错误'); // TypeError
    

2. 键的顺序

  • Object:传统上认为无序,ES6 后对字符串键有特定排序规则,但不保证遍历顺序。
  • Map严格保持键值对的插入顺序,遍历顺序与插入顺序一致。
  • WeakMap:无序,且无法遍历。

3. 大小获取

  • Object:需要手动计算 Object.keys(obj).length
  • Map:直接通过 map.size 获取
  • WeakMap:无法获取大小(因为随时可能有键被垃圾回收)

4. 迭代性

  • Object:需要借助 Object.keys()Object.values()Object.entries()
  • Map直接可迭代,有 forEach() 方法和迭代器
    const map = new Map([['a', 1], ['b', 2]]);
    
    for (let [key, value] of map) {
      console.log(key, value); // a 1, b 2
    }
    
  • WeakMap不可迭代,没有迭代相关的方法

5. 垃圾回收

这是 WeakMap 最独特的地方:

  • Object 和 Map:对键是强引用,即使对象在其他地方不再使用,只要还在 Object/Map 中,就不会被垃圾回收,可能导致内存泄漏。

    let obj = { data: 'important' };
    const map = new Map();
    map.set(obj, 'metadata');
    
    obj = null; // 对象仍然被 Map 引用,不会被回收
    
  • WeakMap:对键是弱引用,不阻止垃圾回收。

    let obj = { data: 'important' };
    const weakMap = new WeakMap();
    weakMap.set(obj, 'metadata');
    
    obj = null; // 如果没有其他引用,对象会被垃圾回收,WeakMap 中的条目自动移除
    

6.使用场景

Object 的使用场景
  • 简单的数据记录(如配置对象)
  • 需要 JSON 序列化的数据结构
  • 方法集合(如工具类)
  • 当键是已知的字符串时
// 配置对象
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

// 工具类
const mathUtils = {
  add: (a, b) => a + b,
  multiply: (a, b) => a * b
};
Map 的使用场景
  • 需要频繁增删键值对的场景
  • 需要保持插入顺序的场景
  • 键类型复杂(需要对象作为键)
  • 需要遍历操作的场景
// 缓存系统
const cache = new Map();

function getData(key) {
  if (cache.has(key)) {
    return cache.get(key);
  }
  const data = fetchDataFromServer(key);
  cache.set(key, data);
  return data;
}

// DOM 节点映射
const nodeMap = new Map();
const buttons = document.querySelectorAll('button');

buttons.forEach(button => {
  nodeMap.set(button, { clickCount: 0 });
});
WeakMap 的使用场景
  • 为对象存储私有数据或元数据
  • DOM 节点关联数据(避免内存泄漏)
  • 缓存系统(自动清理)
// 私有数据存储
const privateData = new WeakMap();

class User {
  constructor(name) {
    privateData.set(this, { name });
  }
  
  getName() {
    return privateData.get(this).name;
  }
}

// DOM 数据关联
const domData = new WeakMap();
const element = document.getElementById('myElement');

domData.set(element, { loaded: false, data: null });

“爱情不是彼此凝视,而是一起朝同一个方向看去。” -- 《小王子》(诠释:真爱意味着拥有共同的目标和愿景,携手同行。)

4. 什么是强引用、弱引用?

这个问题到目前为止,尚未被问到,但是我觉得值得深究一下。 简单做个比喻,想象一下你和对象的关系:

  • 强引用:就像你用绳子紧紧绑住一个气球。只要你抓着绳子,气球就永远不会飞走。
  • 弱引用:就像你用细线轻轻系着气球。如果你放手了,一阵风吹来,气球就可能飞走。 在js中,垃圾回收机制(Garbage Collector, GC)就是那个"风"。

可是爱情不会被这么简单的定义,哪怕你紧紧地攥住那根绳子,一阵子风吹来,她也可以选择飞走。所以,朋友们,别太用力了呀。爱过了头,你会觉得累,她也会倍感压力。

强引用

强引用类型示例特点与用途风险等级
变量声明let obj = {}
const arr = []
最基本的引用类型,用于存储数据和对象引用。作用域结束时自动释放。低(作用域结束时释放)
数组包含arr.push(obj)
array[index] = obj
用于创建有序的对象集合,支持随机访问和迭代操作。中(需要手动清理或移位)
对象属性obj.child = childObj
parent[prop] = value
用于构建复杂的数据结构,对象嵌套和关系表示。中(需要手动置null)
Map集合map.set(key, obj)
map.set('string', obj)
键值对集合,键可以是任意类型,保持插入顺序,支持迭代。高(容易内存泄漏)
Set集合set.add(obj)
new Set([obj1, obj2])
值唯一性集合,用于去重和成员关系检查,支持迭代。高(需要手动delete)
闭包引用function() { use externalVar }函数捕获外部变量,实现数据封装和私有状态维护。中(需要理解作用域链)
DOM引用document.getElementById()
querySelectorAll()
获取和操作DOM元素,用于界面交互和动态内容更新。中(DOM移除后需手动置null)
定时器引用setInterval(callback)
setTimeout(callback)
定时执行代码,回调函数闭包捕获外部变量引用。高(必须clearInterval)
事件监听器element.addEventListener()
eventEmitter.on()
事件处理机制,回调函数保持对相关对象的引用。高(必须removeEventListener)
模块全局变量export const cache = new Map()模块级别的持久化存储,生命周期与程序相同。高(需要设计清理策略)

弱引用

弱引用类型示例特点与用途注意事项
WeakMapweakMap.set(obj, data)对象作为键的弱引用映射,自动清理;用于DOM数据关联、对象元数据存储键必须是对象,不可枚举
WeakSetweakSet.add(obj)弱引用对象集合,自动清理;用于临时对象集合值必须是对象,不可枚举
WeakRefnew WeakRef(obj)创建对象的弱引用,可手动检查;用于缓存系统ES2021+,需要手动调用 deref()
FinalizationRegistrynew FinalizationRegistry(callback)对象被回收时执行回调;用于资源清理ES2021+,用于清理辅助资源

核心特点对比表

特性强引用弱引用
垃圾回收阻止不阻止
访问可靠性绝对可靠不确定
内存泄漏风险(需手动管理)(自动管理)
可枚举性可枚举、可测大小不可枚举、无法获知大小
键/值类型任意类型键必须是对象(WeakMap/WeakSet)
默认性默认引用方式需显式创建
主要用途通用数据存储、长期持有元数据关联、缓存、防止内存泄漏

具体示例对比

强引用示例 - 可能导致内存泄漏
class Cache {
    constructor() {
        this.data = new Map(); // 强引用Map
    }
    
    add(key, value) {
        this.data.set(key, value);
    }
}

let cache = new Cache();
let bigData = new Array(1000000).fill('test'); // 大量数据

cache.add('user1', bigData);

// 即使我们不再需要 bigData...
bigData = null;

// 数据仍然被 cache.data Map 强引用着,无法被回收!
// 这就是内存泄漏
弱引用示例 - 自动内存管理
class SmartCache {
    constructor() {
        this.data = new WeakMap(); // 弱引用WeakMap
    }
    
    add(key, value) {
        this.data.set(key, value);
    }
}

let smartCache = new SmartCache();
let user = { id: 1 }; // 对象作为键
let bigData = new Array(1000000).fill('test');

smartCache.add(user, bigData);

// 当我们不再需要 user 对象时...
user = null;
bigData = null;

// 由于没有其他强引用指向 user 对象,
// 它会被垃圾回收,WeakMap 中的条目也会自动移除
// 内存自动释放!

为什么需要弱引用?

1. 避免内存泄漏
// 传统方式 - 可能内存泄漏
const elementListeners = new Map();
const button = document.getElementById('myButton');

elementListeners.set(button, {
    onClick: () => console.log('clicked'),
    onHover: () => console.log('hovered')
});

// 即使从DOM移除button,数据仍然被Map强引用
// 使用WeakMap - 自动清理
const elementListeners = new WeakMap();
const button = document.getElementById('myButton');

elementListeners.set(button, {
    onClick: () => console.log('clicked'),
    onHover: () => console.log('hovered')
});

// 当button从DOM移除并被垃圾回收时,数据自动清理
2. 缓存实现
const cache = new WeakMap();

function getExpensiveData(obj) {
    if (cache.has(obj)) {
        return cache.get(obj);
    }
    
    const result = expensiveCalculation(obj);
    cache.set(obj, result); // 弱引用,不会阻止obj被回收
    return result;
}
3. 私有属性模拟
const privateData = new WeakMap();

class Person {
    constructor(name) {
        privateData.set(this, { name }); // this是弱引用的键
    }
    
    getName() {
        return privateData.get(this)?.name;
    }
}

let person = new Person('John');
console.log(person.getName()); // "John"

person = null; // Person实例可以被回收,私有数据自动清理

简单记

  • 强引用是"我坚决不放手",弱引用是"你走了我也不留"。
  • 强引用会阻止垃圾回收器回收其引用的对象,只要该强引用本身仍然处于可访问状态,它所指向的对象就会被视为‘存活’状态,从而无法被内存回收机制释放。
  • 弱引用允许其所引用的对象在失去所有强引用后被垃圾回收器正常回收

“也许世界上也有五千朵和你一模一样的花,但只有你是我独一无二的玫瑰。” -- 《小王子》

5. 如何对一个包含相同对象的数组进行去重?(场景题)

方法一:使用 JSON.stringify()(适用于简单对象)

这种方法先将对象转换为字符串,然后去重,最后再转回对象。

const arrayWithDuplicates = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice' }, // 重复对象
  { id: 3, name: 'Charlie' },
  { id: 2, name: 'Bob' }    // 重复对象
];

// 方法1.1:使用 Set 和 JSON.stringify
const uniqueArray = Array.from(
  new Set(arrayWithDuplicates.map(item => JSON.stringify(item)))
).map(item => JSON.parse(item));

console.log(uniqueArray);
// 输出: [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]

// 方法1.2:使用 reduce 和 JSON.stringify
const uniqueArray2 = arrayWithDuplicates.reduce((acc, current) => {
  const stringified = JSON.stringify(current);
  if (!acc.has(stringified)) {
    acc.set(stringified, current);
  }
  return acc;
}, new Map()).values();

console.log(Array.from(uniqueArray2));

⚠️ 局限性:

  • 对象属性的顺序必须完全一致
  • 不能处理包含函数、undefined、循环引用的对象
  • 性能相对较差(需要序列化和反序列化)

方法二:使用自定义比较函数(推荐)

这种方法更灵活,可以自定义比较逻辑。

// 方法2.1:基于特定属性去重
function removeDuplicatesByKey(array, key) {
  const seen = new Set();
  return array.filter(item => {
    const value = item[key];
    if (seen.has(value)) {
      return false;
    }
    seen.add(value);
    return true;
  });
}

// 使用 id 作为去重依据
const uniqueById = removeDuplicatesByKey(arrayWithDuplicates, 'id');
console.log(uniqueById);

// 方法2.2:基于多个属性去重
function removeDuplicatesByKeys(array, keys) {
  const seen = new Set();
  return array.filter(item => {
    const keyValue = keys.map(key => item[key]).join('|');
    if (seen.has(keyValue)) {
      return false;
    }
    seen.add(keyValue);
    return true;
  });
}

// 使用 id 和 name 作为复合键去重
const uniqueByIdAndName = removeDuplicatesByKeys(arrayWithDuplicates, ['id', 'name']);
console.log(uniqueByIdAndName);

方法三:使用 Lodash 等工具库(最方便)

如果你不介意使用第三方库,Lodash 提供了现成的解决方案。

// 首先安装 lodash: npm install lodash
import _ from 'lodash';

// 方法3.1:深度比较去重
const uniqueArray = _.uniqWith(arrayWithDuplicates, _.isEqual);
console.log(uniqueArray);

// 方法3.2:基于属性去重
const uniqueById = _.uniqBy(arrayWithDuplicates, 'id');
console.log(uniqueById);

方法四:通用深度比较去重函数

如果你想要一个不依赖库的完整解决方案:

function deepEqual(obj1, obj2) {
  if (obj1 === obj2) return true;
  
  if (typeof obj1 !== 'object' || obj1 === null || 
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }
  
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  for (const key of keys1) {
    if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
      return false;
    }
  }
  
  return true;
}

function removeDuplicatesDeep(array) {
  return array.filter((item, index, self) => {
    return index === self.findIndex(obj => deepEqual(obj, item));
  });
}

const uniqueArray = removeDuplicatesDeep(arrayWithDuplicates);
console.log(uniqueArray);

方法五:使用 Map 和自定义哈希函数(性能最优)

对于大型数组,这是性能最好的方法。

function createObjectHash(obj) {
  // 创建一个唯一的字符串哈希来表示对象内容
  return Object.entries(obj)
    .sort(([a], [b]) => a.localeCompare(b)) // 排序确保属性顺序不影响哈希
    .map(([key, value]) => `${key}:${typeof value === 'object' ? JSON.stringify(value) : value}`)
    .join('|');
}

function removeDuplicatesWithHash(array) {
  const seen = new Set();
  return array.filter(item => {
    const hash = createObjectHash(item);
    if (seen.has(hash)) {
      return false;
    }
    seen.add(hash);
    return true;
  });
}

const uniqueArray = removeDuplicatesWithHash(arrayWithDuplicates);
console.log(uniqueArray);

总结与推荐

方法优点缺点适用场景
JSON.stringify()简单易用有限制,性能差简单对象,快速原型
自定义比较函数灵活,可定制需要写更多代码按特定属性去重
Lodash最方便,功能强大需要引入外部依赖生产环境,复杂需求
深度比较函数完全控制,无依赖代码复杂,性能中等需要精确控制的场景
哈希函数性能最佳哈希函数需要精心设计大型数据集,性能要求高

推荐选择:

  • 如果是简单需求,用 方法一方法二
  • 如果是生产环境,用 方法三(Lodash)
  • 如果追求最佳性能,用 方法五
  • 如果需要精确控制比较逻辑,用 方法四

“人们已经忘记了这个世界真理,但你不要忘记它。你要永远为你驯服的东西负责。” -- 《小王子》

6. 对一个空数组([])使用 some 方法 会返回什么?

当时被问到这个情况懵了...

最直接的回答:对一个空数组([])使用 some 方法,无论你提供的条件(回调函数)是什么,它都会返回 false。

详细解释和原因

  • Array.prototype.some() 的工作原理: some() 方法用于测试数组中是否至少有一个元素通过了提供的回调函数的测试。一旦找到一个使得回调函数返回“真值”(truthy)的元素,它会立即返回 true 并停止遍历。如果遍历完所有元素都没有找到这样的元素,则返回 false。

  • 数学逻辑: 从逻辑学的角度来看,some()方法是在验证一个命题:“存在一个数组中的元素 X,使得条件函数(X) 为真”。

    • 对于一个非空数组,我们需要检查其中的元素来证明或否定这个命题。
    • 对于一个空数组,这个“存在”的命题没有任何对象可以来验证它。在数学和逻辑学中,这种“断言一个空集合中存在某元素”的命题被自动认为是假(false)的。因为你根本无法找到一个元素来证明它为真。
  • 规范定义: ECMAScript 语言规范明确定义了这种行为。根据规范,some 方法在遇到空数组时,根本不会执行你提供的回调函数,而是直接返回 false


last 彩蛋

以前的我总想要一个结果,这样的心态往往会导致忽略了过程。但其实**过程正是由每一个小小的结果所组成。**希望大家不要在追求结果的过程中迷了路!

最后我想说爱的核心是责任、理解、付出和独一无二的羁绊,愿你我珍惜眼前人,珍惜每一个当下,共勉!