目录:
1.什么是函数防抖和函数节流?有什么区别?
2.什么是函数柯里化及有什么用?
3.谈谈模块化开发
4.谈谈你对作用域链的理解
5.什么是深拷贝?什么是浅拷贝?如何实现一个深拷贝函数?
1.什么是函数防抖和函数节流?有什么区别?
【函数防抖(debounce)】,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
也就是说在连续触发一个事件时,只执行最后一个
防抖常见场景
连续的事件,只需触发一次回调的场景有:
搜索框搜索输入。只需用户最后一次输入完,再发送请求
手机号、邮箱验证输入检测
窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
【函数节流(throttle)】,限制一个函数在一定时间内只能执行一次。
节流常见场景
间隔一段时间执行一次回调的场景有:
滚动加载,加载更多或滚到底部监听
谷歌搜索框,搜索联想功能
高频点击提交,表单重复提交
相同点:
都可以通过使用 setTimeout 实现。
目的都是,降低回调执行频率。节省计算资源。
不同点:
函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 实现。一定时间连续触发,只在最后执行一次,
函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
2.什么是函数柯里化及有什么用?
3.谈谈模块化开发
模块化开发就是封装细节,提供使用接口,彼此之间互不影响,每个模块都是实现某一特定的功能。
模块化开发的基础就是函数
模块化的规范
1.CommonJS规范
该规范的核心思想是允许模块通过require方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口。
优点:
1、简单并容易使用
2、服务器端模块便于重用
缺点:
1、同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
2、不能非阻塞的并行加载多个模块
2.AMD规范
由于浏览器端的模块不能采用同步的方式加载,会影响后续模块的加载执行,因此AMD(Asynchronous Module Definition异步模块定义)规范诞生了。
AMD标准中定义了两个API
1、require([module], callback);用来加载一系列模块
2、define(id, [depends], callback);用来定义并暴露一个模块。
优点:
1、适合在浏览器环境中异步加载模块
2、可以并行加载多个模块
缺点:
1、提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
2、不符合通用的模块化思维方式,是一种妥协的实现
3.CMD规范
CMD(Common Module Definition)规范和AMD很相似,尽量保持简单,并与CommonJS和Node.js的 Modules 规范保持了很大的兼容性。
在CMD规范中,一个模块就是一个文件。
优点:
1、依赖就近,延迟执行
2、可以很容易在 Node.js 中运行
缺点:
1、依赖 SPM 打包,模块的加载逻辑偏重
AMD和CMD的区别
1、对于依赖的模块,AMD是提前执行,CMD是延迟执行。
2、AMD推崇依赖前置;CMD推崇依赖就近,只有在用到某个模块的时候再去require
3、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
4.ES6模块化
优点:
1、容易进行静态分析
2、面向未来的 EcmaScript 标准
缺点:
1、原生浏览器端还没有实现该标准
2、全新的命令字,新版的 Node.js才支持
参考:如何正确理解js的模块化
4.谈谈你对作用域链的理解
作用域链是 JavaScript 中用于变量查找的核心机制,它决定了在当前作用域中找不到某个变量时,引擎如何向上层作用域逐级搜索,直到找到该变量或抛出错误。
作用域链的形成时机:函数在定义时就确定了其作用域链,而非调用时。它由函数创建时所在作用域中的可访问数据对象集合构成
查找规则:当访问一个变量时,JavaScript 引擎会从当前作用域开始查找;若未找到,则沿作用域链逐级向上(向父级、祖父级……)查找;直到全局作用域;若全局作用域仍找不到,则抛出ReferenceError
- 链式结构特征:嵌套函数会包含外层函数的作用域,形成多层链式结构;内部可访问外部,外部不可访问内部
- 与闭包的关系:作用域链是闭包实现的基础。即使外层函数执行完毕,其变量对象仍可能被内层函数通过作用域链引用而保留,从而避免被垃圾回收
- 作用域又分为:全局作用域,函数作用域,块级作用域。
1.全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
2.函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
3.块级作用域
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量 - 动态作用域和词法作用域的区别
词法作用域:函数的作用域由定义位置决定。
动态作用域:函数的作用域由调用位置决定
比如:
var value = "global";
function foo() {
console.log(value);
}
function bar() {
var value = "local";
foo(); // 输出什么?
}
bar();
(词法作用域下),上述代码的输出结果是:global。因为 foo() 函数在定义时,它的作用域链就已确定,包含全局作用域。所以它访问的是全局的 value 变量,而不是调用位置的 value 。
如果是动态作用域呢?会输出什么?
动态作用域下:访问调用位置的value,则输出local
5.什么是深拷贝?什么是浅拷贝?如何实现一个深拷贝函数?
浅拷贝是指创建一个新对象,新对象的属性是对原对象属性值的引用。对于基本数据类型(如字符串、数字、布尔值等),会拷贝值;但对于引用数据类型(如对象、数组等),靠背的是内存地址,这意味着新对象和原对象的引用数据类型属性指向同一块内存空间,修改其中一个对象的引用属性,另一个对象也会受影响。
例如:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = Object.assign({}, obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3
Object.assign 就是一种浅拷贝方式。
深拷贝则是创建一个新对象,新对象的所有属性和子属性都与原对象完全独立,互不影响。也就是说,对于引用数据类型,不仅会创建新的对象,其内部嵌套的所有引用类型也会递归的创建新对象。
实现深拷贝可以使用递归的方式,例如:
function deepClone(obj) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
let newObj = Array.isArray(obj)? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
let original = { a: 1, b: { c: 2 } };
let cloned = deepClone(original);
cloned.b.c = 3;
console.log(original.b.c); // 输出 2
使用 hasOwnProperty 确保是自身属性而非原型链上的属性
深拷贝的局限性
1.循环引用问题:如果对象中存在循环引用,即对象A的属性引用了对象A本身,或者多个对象之间形成了环形引用,这个函数会陷入无限递归,导致栈溢出错误。例如:
let a = {};
a.self = a;
let clone = deepClone(a);
// 这里调用deepClone会导致栈溢出
2.无法处理特殊对象:像 Date、RegExp、Function 等特殊对象类型,上述函数处理得并不准确。对于 Date 对象,深拷贝后应该是一个新的 Date 实例且值相同,但上述函数只是简单地将其作为普通对象处理,新对象不再是 Date 类型。对于 Function,深拷贝的意义不大且难以准确实现,因为函数在不同执行环境下可能有不同行为,但上述函数同样会尝试递归处理函数内部的对象属性等,这并非预期行为。
3.属性描述符丢失:它只复制了可枚举的自身属性值,对象的属性描述符(如 writable、configurable、enumerable 等)和访问器属性(getter 和 setter)不会被复制。例如:
let obj = {
get prop() {
return 'value';
}
};
let cloned = deepClone(obj);
// cloned.prop 不再是访问器属性,而是 undefined