当前位置: 首页 > news >正文

面试笔记-js基础篇

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 运算符的内部工作机制如下:

  1. 获取 constructorprototype 属性值,记为 prototypeObj
  2. 获取 object 的原型链(__proto__Object.getPrototypeOf(object))。
  3. 在原型链中从 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
  • 不能通过 callapplybind 改变箭头函数的 this 对于箭头函数,这些方法对 this 的绑定没有效果。
  • 没有 arguments 对象: 箭头函数没有自己的 arguments 对象,但你可以使用 rest 参数 (...args) 来获取所有传入的参数。
  • 不能作为构造函数(没有 new 绑定): 你不能使用 new 关键字来调用箭头函数,否则会抛出错误。

8、call、apply、bind

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,避免使用 topleft 等定位属性,使用 transform 结合 translate 来改变位置,不会引发重排,只会引发合成(compositing)重绘。

  • 使用Document Fragment或隐藏元素修改:在更新多个 DOM 时,使用 DocumentFragment 或者将元素 display: none 之后再进行批量更新,完成后再显示,这样只会触发一次重排和重绘。

  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

13、如何渲染几万条数据并不卡住界面

  • 虚拟列表
  • 懒加载
  • requestAnimationFrame

14、关于执行上下文

执行上下文定义了代码的执行环境,包括变量、函数和对象的可访问性。执行上下文可以帮助我们理解作用域、变量提升以及 this 的指向等概念。

执行上下文的类型可以分为:

  1. 全局上下文;在全局上下文中定义的变量和函数是全局可访问的
  2. 函数上下文;函数上下文中包含该函数的参数、局部变量和对外部变量的访问权限。
  3. Eval 上下文;使用 eval 语句时会创建一个特殊的执行上下文。谨慎使用eval,会带来性能和安全隐患
  • 性能问题。eval 会使 JavaScript 引擎无法进行优化,因为它需要在运行时解析和执行代码。这可能导致性能下降,尤其是在大规模使用时。
  • 安全隐患。使用 eval 可能导致安全问题,特别是在处理不可信的输入时。如果用户输入的字符串被直接传递给 eval,可能会导致代码注入攻击。

执行上下文由三个部分组成:

  • 变量环境;存储该上下文中定义的变量和函数声明;
  • 作用域链;指的是代码中可以访问变量和函数的区域。JavaScript 中主要有两种作用域:全局作用域和局部作用域。JavaScript 采用的是静态作用域,函数的作用域在函数定义的时候就决定了。
  • this 关键字的指向;

15、闭包

闭包:就是可以访问外部作用域变量的内部函数

注意事项

虽然外部函数已经执行完成,但是变量仍然存在于内存中,因为闭包保持着对它们的引用,被引用的变量直到闭包被销毁时才会被销毁。可能导致对象无法被垃圾回收机制回收,从而导致内存泄漏(内存泄漏是指程序在运行时未能正确释放不再需要的内存空间,导致这部分内存无法被重新利用。这通常会导致应用程序的内存使用不断增加,最终可能导致性能下降或崩溃。)

使用场景

  1. 数据封装与私有变量:使用闭包可以创建具有私有变量的对象,控制对数据的访问。

  2. 防止全局命名冲突:闭包可以用来封装代码,防止全局作用域被污染。

  3. 函数记忆:闭包可以用于生成带有特定环境的函数(函数工厂),这些函数可以记住其创建时的上下文。

比较常见的节流和防抖的函数就是利用了函数记忆这个特点

防抖函数

// 防抖一般用于防止按钮被多次点击,从而频繁触发点击时间。防抖的处理思想是,一段时间内,无论操作多少次,都只执行最后一次function debounce(fn, wait) {let timer;return function (...args) {if (timer) clearTimeout(timer)const context = thistimer = setTimeout(() => {fn.apply(context, args)}, wait)}
}

节流函数

// 节流的原理是一段时间内频繁触发某件事,只会执行一次,用于减少浏览器开销。常见的场景有 滚动事件:滚动页面时频繁触发 scroll 事件,节流可以确保在每隔一定时间内执行一次,避免性能问题// 最简单的实现方式是时间戳function throttle(fn, delay) {let lastTime = 0return function (...args) => {const now = Data.now()const context = thisif (now - lastTime >= delay) {lastTime = nowfn.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):标记-清除算法在执行时,必须暂停程序的执行,这意味着程序会在垃圾回收期间停止响应(虽然现代垃圾回收器通过分代回收、并发和增量标记来优化这种情况)。
  • 标记和清除的性能问题:每次垃圾回收时,都会遍历整个内存空间的对象,时间复杂度与堆中的对象数量成正比,随着对象数量的增加,性能可能下降。

现代垃圾回收器通过各种优化技术,如分代回收、增量回收和并发回收,来减少标记-清除算法带来的性能问题。

  1. 分代回收:将对象按生命周期分为“新生代”和“老生代”。新生代对象往往生命周期短,因此垃圾回收会频繁检查新生代中的对象。而老生代对象生命周期长,垃圾回收检查的频率较低。
  2. 增量回收:将一次完整的垃圾回收拆分成多个小步骤,每次只进行一部分回收工作,以减少“暂停-世界”的时间。
  3. 并发回收:在现代浏览器中,垃圾回收器可以利用多核 CPU,并发处理标记和清除的工作,进一步减少对主线程的影响。

例子:

假设我们有以下代码:

function outer() {let obj1 = { name: "object 1" };let obj2 = { name: "object 2" };function inner() {console.log(obj1);}return inner;
}const closure = outer();

在这个例子中:

  • obj1outer 函数的局部变量,它被 inner 函数引用。因此,尽管 outer 执行结束后,obj1 没有从根对象直接引用,但由于它被闭包引用,所以 obj1 仍然是可达的,不会被回收。
  • obj2 没有被 inner 函数引用,也没有其他地方引用它,因此 obj2outer 函数执行结束后,将在下一次垃圾回收时被回收。

标记-清除算法的回收流程将标记 obj1 为可达对象,而 obj2 则会被清除。

1)什么时候发生垃圾回收?

浏览器通常会在以下几个场景执行垃圾回收:

  • 内存占用达到某个阈值时。
  • 程序进入空闲阶段(例如 JavaScript 代码执行完毕后,等待用户输入或其他事件时)。
  • 显示的浏览器空闲时段(Idle Periods)。

2)内存泄漏的常见原因

尽管浏览器有垃圾回收机制,但仍可能发生内存泄漏,主要原因包括:

  1. 全局变量未释放:全局变量会一直存在于全局执行上下文,难以回收。
  2. 被遗忘的定时器或事件监听器:未清除的 setIntervalsetTimeout 或未移除的事件监听器,仍然引用着对象,导致对象无法被回收。
  3. 闭包:闭包会保持对外部变量的引用,如果这些变量不再需要,但依旧被引用,内存就无法释放。

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))
  • 无法通过callapplybind绑定箭头函数的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);


http://www.mrgr.cn/news/46345.html

相关文章:

  • 调用CString::Format接口格式化字符串时产生异常,可能是将当前的CString对象作为参数传给CString::Format接口导致的
  • Transform(二)
  • jwt跨域认证
  • 制造业人工智能的场景应用落地现状、难点和建议
  • Linux基础入门 --17 DAY(软件包管理)
  • 源代码泄密防护系统有哪些?这7款源代码泄密防护系统,企业管理者的福音!
  • apt update报错:ModuleNotFoundError: No module named ‘apt_pkg‘(可能是默认python版本被改坏了)
  • 【10086网上营业厅-注册/登录安全分析报告】
  • RabbitMQ延迟队列
  • 21.数据结构与算法-遍历二叉树/三种遍历算法/递归遍历/非递归遍历/建立,复制二叉树/求二叉树的深度,节点个数,叶子节点个数
  • 嵌入式仿真实验教学平台
  • 理解Web3的互操作性:不同区块链的连接
  • 鸿蒙开发(NEXT/API 12)【ArkWeb接入密码保险箱】系统安全
  • 最好的超声波清洗机是哪款牌子?四款顶流超声波清洗机强烈推荐!
  • java实现桌面程序开机自启动
  • postman变量,断言,参数化
  • sass学习笔记(1.0)
  • AI工程师:AI时代的新岗位
  • Python 语法及入门(超全超详细)!
  • 【RAG论文精读5】RAG论文综述1(2312.10997)-第3部分:检索器