1、因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000
开头代表是对象,然而 null
表示为全零,所以将它错误的判断为 object
。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
typeof null // 'object'
2、最准确判断类型的是 Object.prototype.toString.call(null) === '[object Type]'
注意type的第一个字母为大写
3、判断两个值是否相等
null可以用 null === null以及Object.is(null, null)
NaN需要用Object.is(NaN, NaN)
undefined,一般情况下使用undefined === undefined判断,但在一些古早的老版本的浏览器中undefined不是保留词,可能会存在undefined
变量被重新赋值。可以使用undefined === void 0,
void 0 总会安全地返回undefined
4、Boolean把 null,undefined,false,NaN,'', +/-0转化为false,其他值包括对象都转化为true
5、instanceof通常用于检查一个对象是否是某个构造函数的实例。它在判断某个对象是否属于特定类型时非常有用,但它主要适用于引用类型(对象)。对于基本类型(如字符串、数字、布尔值等),instanceof
不适用,因为它们不是对象。
instanceof
运算符的内部工作机制如下:
- 获取
constructor
的prototype
属性值,记为prototypeObj
。 - 获取
object
的原型链(__proto__
或Object.getPrototypeOf(object)
)。 - 在原型链中从
object
开始逐级向上查找。- 如果找到了一个原型等于
prototypeObj
,返回true
。 - 如果到达原型链的顶端(即
null
),仍然没有找到,返回false
。
- 如果找到了一个原型等于
当我们运行 foo instanceof Foo
时:
- JavaScript 首先检查
foo.__proto__
是否等于Foo.prototype
,是的话返回true
。 - 如果不是,继续检查
foo.__proto__.__proto__
是否等于Foo.prototype
,直到找到为止。 - 如果到达了原型链的顶端(
null
)仍然没有找到Foo.prototype
,则返回false
。
6、原型链:在 JavaScript 中,每个对象都有一个内部属性([[Prototype]]
),它指向其构造函数的原型对象(即 prototype
)。这就形成了一条“原型链”。当我们访问对象的一个属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null
)为止。
7、箭头函数的this:
this
是静态的,取决于函数定义时的上下文: 箭头函数不会有自己的this
,它会捕获其所定义时所在上下文的this
。- 不能通过
call
、apply
或bind
改变箭头函数的this
: 对于箭头函数,这些方法对this
的绑定没有效果。 - 没有
arguments
对象: 箭头函数没有自己的arguments
对象,但你可以使用rest
参数 (...args
) 来获取所有传入的参数。 - 不能作为构造函数(没有
new
绑定): 你不能使用new
关键字来调用箭头函数,否则会抛出错误。
8、call、apply、bind
bind与call、apply最大的不同:
当我们把一个绑定函数用作构造函数时,bind
的行为会发生变化。具体来说,当一个绑定函数被用作构造函数调用时:
- 忽略绑定时传入的上下文:绑定函数会忽略你在
bind
时传入的this
上下文,而是将this
指向新创建的实例对象。 - 保持原函数的原型链:绑定函数创建的实例对象会继承原始函数(未绑定的函数)的原型链,这意味着实例对象可以访问原始函数的所有方法和属性。
// 手写call
Function.prototype.call2 = function(context, ...args) {
context = context || window
context.fn = this
const result = context.fn(args)
delete context.fn
return result
}
// 手写apply
Function.prototype.apply2 = function(context, args) {
context.fn = this
let result
// args为数组或为空
if (args) result = context.fn(...args)
else result = context.fn()
delete context.fn
return result
}
/** 手写bind
* bind实现有两个比较关键的点;
* 1、参数柯里化(即绑定时传入的参数和调用时传入的参数可以合并到原函数)
* 2、当把返回的函数作为构造函数时,
* 1)会忽略bind时传入的this上下文,而是将this指向新创建的实例对象;
* 2)保持原函数的原型链,既实例对象可以访问原始函数的所有方法和属性
**/
Function.prototype.bind2 = function(context, ...args) {
let self = this
const foundFun = function(...args1) {
const isConstructor = this instanceOf foundFun // 判断当前是否作为构造函数被调用
const bindArgs = isConstructor ? this : context // 如果是构造函数就指向构造函数调用时的this
self.apply(bindArgs, [...args, ...args1])
}
// 还需要保持原函数的原型链,让其可以访问原始函数的方法与属性
foundFun.prototype = Object.create(self.prototype) // 使用Object.create是因为它会创建了一个新对象,保证foundFun和Object互相独立,新增属性等情况时不会互相干扰
return foundFun
}
9、setTimeout可以传入多个参数
function greet(name, message) {
console.log(name + ": " + message);
}
// 延迟 2 秒后执行函数,并传递参数 "Alice" 和 "Hello!"
setTimeout(greet, 2000, "Alice", "Hello!");
传递给回调函数的参数
- 从第三个参数开始,可以传递任意数量的参数,这些参数会作为回调函数的参数传入。
10、深浅拷贝
浅拷贝的多种方式。Object.assign()、扩展运算符(...
)、slice()。
浅拷贝只能解决第一层的问题,如果需要解决多层拷贝的问题,就需要用到深拷贝
深拷贝通常可以通过 JSON.parse(JSON.stringify(object))
来解决。
但是该方法也是有局限性的:
- 会忽略
undefined
- 会忽略
symbol
- 不能序列化函数
- 不能解决循环引用的对象
11、三种模块化处理的区别
- commonjs是同步导入,适合服务端,ES Modules是异步导入,适合浏览器端
- commonjs导出的时候是对值的拷贝,ES Modules导出的是对值的引用
- commonjs在第一次加载时被执行,并缓存其导出结果。ES Modules在导入时不会立即执行,而是在需要时进行异步加载和执行
随着 ES Modules (ESM) 成为 JavaScript 的标准模块系统,以及现代打包工具(如 Webpack)的使用,AMD 的使用场景有所减少。
12、如何避免频繁重排和重绘
-
使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局) -
使用
translate
替代top,
避免使用top
、left
等定位属性,使用transform
结合translate
来改变位置,不会引发重排,只会引发合成(compositing)重绘。 -
使用Document Fragment或隐藏元素修改:在更新多个 DOM 时,使用
DocumentFragment
或者将元素display: none
之后再进行批量更新,完成后再显示,这样只会触发一次重排和重绘。 -
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
-
动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
13、
如何渲染几万条数据并不卡住界面
- 虚拟列表
- 懒加载
- requestAnimationFrame
14、关于执行上下文
执行上下文定义了代码的执行环境,包括变量、函数和对象的可访问性。执行上下文可以帮助我们理解作用域、变量提升以及 this
的指向等概念。
执行上下文的类型可以分为:
- 全局上下文;在全局上下文中定义的变量和函数是全局可访问的
- 函数上下文;函数上下文中包含该函数的参数、局部变量和对外部变量的访问权限。
- Eval 上下文;使用
eval
语句时会创建一个特殊的执行上下文。谨慎使用eval,会带来性能和安全隐患
- 性能问题。
eval
会使 JavaScript 引擎无法进行优化,因为它需要在运行时解析和执行代码。这可能导致性能下降,尤其是在大规模使用时。 - 安全隐患。使用
eval
可能导致安全问题,特别是在处理不可信的输入时。如果用户输入的字符串被直接传递给eval
,可能会导致代码注入攻击。
执行上下文由三个部分组成:
- 变量环境;存储该上下文中定义的变量和函数声明;
- 作用域链;指的是代码中可以访问变量和函数的区域。JavaScript 中主要有两种作用域:全局作用域和局部作用域。JavaScript 采用的是静态作用域,函数的作用域在函数定义的时候就决定了。
this
关键字的指向;
15、闭包
闭包:就是可以访问外部作用域变量的内部函数
注意事项
虽然外部函数已经执行完成,但是变量仍然存在于内存中,因为闭包保持着对它们的引用,被引用的变量直到闭包被销毁时才会被销毁。可能导致对象无法被垃圾回收机制回收,从而导致内存泄漏(内存泄漏是指程序在运行时未能正确释放不再需要的内存空间,导致这部分内存无法被重新利用。这通常会导致应用程序的内存使用不断增加,最终可能导致性能下降或崩溃。)
使用场景
-
数据封装与私有变量:使用闭包可以创建具有私有变量的对象,控制对数据的访问。
-
防止全局命名冲突:闭包可以用来封装代码,防止全局作用域被污染。
-
函数记忆:闭包可以用于生成带有特定环境的函数(函数工厂),这些函数可以记住其创建时的上下文。
比较常见的节流和防抖的函数就是利用了函数记忆这个特点
防抖函数
// 防抖一般用于防止按钮被多次点击,从而频繁触发点击时间。防抖的处理思想是,一段时间内,无论操作多少次,都只执行最后一次
function debounce(fn, wait) {
let timer;
return function (...args) {
if (timer) clearTimeout(timer)
const context = this
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
节流函数
// 节流的原理是一段时间内频繁触发某件事,只会执行一次,用于减少浏览器开销。常见的场景有 滚动事件:滚动页面时频繁触发 scroll 事件,节流可以确保在每隔一定时间内执行一次,避免性能问题
// 最简单的实现方式是时间戳
function throttle(fn, delay) {
let lastTime = 0
return function (...args) => {
const now = Data.now()
const context = this
if (now - lastTime >= delay) {
lastTime = now
fn.apply(context, args)
}
}
}
看下以下的执行顺序
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
第一个输出0,1,2,3,4;第二个输出5,5,5,5,5
原因:
let是块级作用域,这意味着每次迭代时都会创建一个新的 i
变量实例,所以每一次循环的i
其实都是一个新的变量。你可能会问,如果每一轮循环的变量i
都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。
var
声明的变量,作用域是整个包含它的函数。如果它在全局上下文中声明,则会成为全局变量;如果在某个函数内部,它的作用域就是该函数。意味着在整个循环中只有一个 i
变量实例。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值
16、垃圾回收机制
浏览器的垃圾回收机制主要用于管理内存,确保在执行Javascript时不会再出现内存泄漏或资源浪费。垃圾回收机制的核心是通过识别那些对象不再被使用,将它从内存中清除。JS使用的是自动垃圾回收。主要的方式是标记清除。
标记清除通过“标记”和“清除”两个阶段来识别并清除不再需要的对象,核心思想是 从根对象开始,标记所有仍然可访问的对象,未被标记的对象则视为“垃圾”并进行清除。
缺点:
- “暂停-世界”现象(Stop-the-world):标记-清除算法在执行时,必须暂停程序的执行,这意味着程序会在垃圾回收期间停止响应(虽然现代垃圾回收器通过分代回收、并发和增量标记来优化这种情况)。
- 标记和清除的性能问题:每次垃圾回收时,都会遍历整个内存空间的对象,时间复杂度与堆中的对象数量成正比,随着对象数量的增加,性能可能下降。
现代垃圾回收器通过各种优化技术,如分代回收、增量回收和并发回收,来减少标记-清除算法带来的性能问题。
- 分代回收:将对象按生命周期分为“新生代”和“老生代”。新生代对象往往生命周期短,因此垃圾回收会频繁检查新生代中的对象。而老生代对象生命周期长,垃圾回收检查的频率较低。
- 增量回收:将一次完整的垃圾回收拆分成多个小步骤,每次只进行一部分回收工作,以减少“暂停-世界”的时间。
- 并发回收:在现代浏览器中,垃圾回收器可以利用多核 CPU,并发处理标记和清除的工作,进一步减少对主线程的影响。
例子:
假设我们有以下代码:
function outer() {
let obj1 = { name: "object 1" };
let obj2 = { name: "object 2" };
function inner() {
console.log(obj1);
}
return inner;
}
const closure = outer();
在这个例子中:
obj1
是outer
函数的局部变量,它被inner
函数引用。因此,尽管outer
执行结束后,obj1
没有从根对象直接引用,但由于它被闭包引用,所以obj1
仍然是可达的,不会被回收。obj2
没有被inner
函数引用,也没有其他地方引用它,因此obj2
在outer
函数执行结束后,将在下一次垃圾回收时被回收。
标记-清除算法的回收流程将标记 obj1
为可达对象,而 obj2
则会被清除。
1)什么时候发生垃圾回收?
浏览器通常会在以下几个场景执行垃圾回收:
- 内存占用达到某个阈值时。
- 程序进入空闲阶段(例如 JavaScript 代码执行完毕后,等待用户输入或其他事件时)。
- 显示的浏览器空闲时段(Idle Periods)。
2)内存泄漏的常见原因
尽管浏览器有垃圾回收机制,但仍可能发生内存泄漏,主要原因包括:
- 全局变量未释放:全局变量会一直存在于全局执行上下文,难以回收。
- 被遗忘的定时器或事件监听器:未清除的
setInterval
、setTimeout
或未移除的事件监听器,仍然引用着对象,导致对象无法被回收。 - 闭包:闭包会保持对外部变量的引用,如果这些变量不再需要,但依旧被引用,内存就无法释放。
17、this
函数的this完全取决于调用时的上下文决定。
而
箭头函数则比较特殊,是根据定义时的上下文。这意味着箭头函数不会创建自己的 this
,它会捕获并继承外部作用域的 this
值。
下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this
的指向。箭头函数里面根本没有自己的this
,而是引用外层的this
。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
看下以下代码的输出:
var name = 'window';
var student = {
name: '若川',
doSth: function(){
// var self = this;
var arrowDoSth = () => {
// console.log(self.name);
console.log(this.name);
}
arrowDoSth();
},
arrowDoSth2: () => {
console.log(this.name);
}
}
student.doSth(); // '若川'
student.arrowDoSth2(); // 'window'
其实就是相当于箭头函数外的this
是缓存的该箭头函数上层的普通函数的this
。如果没有普通函数,则是全局对象(浏览器中则是window
)。
使用箭头函数有几个需要注意的点:
- 不能用作构造函数,无
new
关键字 - 不具备
arguments
:箭头函数没有自己的arguments
对象,如果要用,可以用 rest 参数代替(即在箭头函数定义时(...arg)) - 无法通过
call
、apply
、bind
绑定箭头函数的this
(它自身没有this
)
function Person(name) {
this.name = name;
}
const person1 = new Person('Alice');
const person2 = Person('Bob');
console.log(person1.name);
console.log(person2);
console.log(name);