跟chatGPT一起复习前端 —— JavaScript

165 阅读32分钟

前言

又到一波面试准备的时刻,整理了一波前端相关要点内容,配合chatGPT完成要点内容整理,有纠正错误和需要补充的小伙伴可以在这里留言,及时更新。

let 、const 与 var的区别

  • var 声明的变量属于函数作用域,而 let 和 const 声明的变量属于块级作用域。
  • var 存在变量提升现象,而 let 和 const 没有此类现象。
  • 在同一个块级作用域中,let 变量不能重新声明,而 var 变量可以重复声明。
  • 在同一个块级作用域中,const 变量不能修改。
  • 在同一个块级作用域中,如果使用了 let 或者 const 声明变量,则必须先声明再使用。

函数作用域与块级作用域区别

在 JavaScript 中,函数作用域和块级作用域是两种不同的作用域。函数作用域是指变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。而块级作用域是指变量在声明它们的块(通常是指 {} 内部)中都是有定义的。

函数作用域和块级作用域之间的区别在于,函数作用域只在函数内部有效,而块级作用域只在块内部有效。因此,使用 var 声明的变量属于函数作用域,而使用 let 和 const 声明的变量属于块级作用域。

// 函数作用域
function test (){
    console.log('函数内var声明的:', a); // 函数内var声明的: undefined
    var a = 'testa';
    console.log('函数内let声明的:', b); // 报错
    let b = 'testb'
}
console.log('函数外var声明的:', a); // 报错 a is not defined
test();

// 块作用域
if (true) {
    let c = 'testc';
    console.log('块作用域里面使用', d); // 块作用域里面使用 undefined
    var d = 'testd';
}
console.log('块作用域外面使用var', d); // 块作用域外面使用var testd
console.log('块作用域外面使用let', c); // 报错

JavaScript有几种数据类型

JavaScript中有7种数据类型,其中6种是基本数据类型,1种是引用数据类型。

基本数据类型:

  • 字符串(String)
  • 数字 (Number)
  • 布尔 (Boolean)
  • 空(Null)
  • 未定义(Undefined)
  • Symbol

引用数据类型:

  • 对象 (Object)
  • 数组 (Array)
  • 函数 (Function)
  • 正则(RegExp)
  • 日期(Date)

Symbol类型是什么?

Symbol是JavaScript中的一种基本数据类型,它是ES6新增的数据类型之一。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:“new Symbol()”。

声明方式:const s = Symbol(“str”)

Symbol类型属于JavaScript中两大数据类型中的基本数据类型。它表示独一无二的值,最大的用法是用来定义对象的唯一属性名。

JavaScript 中如何使用 Set?

在 JavaScript 中,Set 对象是一种有序列表,其中含有一些相互独立的非重复值。Set 对象中的值按插入顺序迭代元素。可以通过以下方式创建 Set 对象:

const set1 = new Set([1, 2, 3]);
console.log(set1); // Set(3) {1, 2, 3}

Set 对象有以下常用方法:

  • add(value):向 Set 对象添加一个新元素。
  • clear():移除 Set 对象内的所有元素。
  • delete(value):从 Set 对象中删除指定元素。
  • has(value):返回一个布尔值,表示该值在 Set 对象中是否存在。
  • size:返回 Set 对象中元素的数量。

JavaScript 中如何使用Map?

在 JavaScript 中,Map 对象是一种简单的键/值映射。任何值(对象或者原始值)都可以作为一个键或一个值。可以通过以下方式创建 Map 对象:

const map1 = new Map();
map1.set('key1', 'value1');
map1.set('key2', 'value2');

Map 对象有以下常用方法:

set(key, value):向 Map 对象添加一个新元素。 clear():移除 Map 对象内的所有元素。 delete(key):从 Map 对象中删除指定元素。 has(key):返回一个布尔值,表示该键在 Map 对象中是否存在。 get(key):返回与指定键相关联的值。

类型Set 与 WeakSet 、Map 与 WeakMap

在 JavaScript 中,Set 和 WeakSet 都是用于存储一组唯一值的对象。它们之间的区别在于 WeakSet 对象中储存的对象值都是被弱引用的,因此这些对象将会被垃圾回收掉,而 Set 对象中储存的对象则不会被垃圾回收掉。

Map 和 WeakMap 都是用于存储键值对的对象。它们之间的区别在于 WeakMap 对象中只能使用对象作为键名,而 Map 对象则可以使用任何类型的值作为键名。WeakMap 对象中储存的对象值都是被弱引用的,因此这些对象将会被垃圾回收掉,而 Map 对象中储存的对象则不会被垃圾回收掉。

类型判断有哪些方法?怎么判断

JavaScript中有多种类型判断方法,以下是其中的几种:

  1. typeof

    • 可以判断基本数据类型,但是对于引用数据类型,除了function都会返回"object"。
    • 例如:typeof “hello” // “string”
  2. instanceof

    • 可以判断一个对象是否是某个构造函数的实例。
    • 例如:[] instanceof Array // true
  3. Object.prototype.toString.call()

    • 可以判断基本数据类型和引用数据类型。
    • 例如:Object.prototype.toString.call(“hello”) // “[object String]”
  4. constructor

    • 可以判断一个对象的构造函数。
    • 例如:[].constructor === Array // true
  5. Array.isArray()

    • 可以判断一个对象是否是数组。
    • 例如:Array.isArray([]) // true

判断是否为数组的几种方式

Array.isArray()、instanceof、Object.prototype.toString.call()、constructor

null 和 undefined 的区别

在JavaScript中,null和undefined都表示没有值。它们之间的区别在于它们的类型不同。null是一个表示“无”值的对象,而undefined是一个表示“无”值的原始值。当您声明一个变量但未将其初始化时,它的默认值为undefined。例如:

let x;
console.log(x); // Output: undefined

另一方面,null用于表示对象为空或不存在。例如:

const obj = null;
console.log(obj); // Output: null

typeof NaN 的结果是什么

在 JavaScript 中,NaN 的 typeof 结果是 number。虽然它 “不是一个数字”,但是 NaN 的 typeof 结果却是 number。这是因为在计算机中,NaN 实际上是一个数值数据类型,但它的值不能用实际数字表示。

isNaN与Number.isNaN函数的区别

在 JavaScript 中,isNaN() 和 Number.isNaN() 都是用来判断一个值是否为 NaN 的函数。但是它们之间有一些区别。Number.isNaN() 函数不存在类型转换的行为,而 isNaN() 会尝试将参数转换成 Number 类型。因此,Number.isNaN() 相比全局函数 isNaN() 更为精准,只有在参数本身为 NaN 的情况下才会返回 true。

{} 和 [] 的 valueOf 和 toString 的结果是什么

在 JavaScript 中,{} 和 [] 都是对象。valueOf 和 toString 都是 Object.prototype 上的方法。当 {} 和 [] 调用 valueOf 方法时,返回对象本身。当它们调用 toString 方法时,返回格式类似于 [object Type]的字符串,其中{Type} 是对象类型。某些对象会重写自身的 toString 方法和 valueOf 方法。

原型、原型链

原型是 JavaScript 中的一个对象,它是构造函数的一个属性,而原型链是由每个对象的原型属性指向其原型对象,直到最后指向 Object.prototype。

JavaScript 中的每个对象都有一个原型对象与之关联,这个原型对象也是一个普通对象,这个普通对象也有自己的原型对象,这样层层递进,就形成了一个链条,这个链条就是原型链。

作用域、作用域链

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。JavaScript 中的作用域是词法作用域,也就是静态作用域,函数的作用域在函数定义的时候就已经确定了。作用域链是由子级作用域返回父级作用域中寻找变量,就叫做作用域链。

怎么理解闭包

闭包是指一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

你可以把闭包理解成“定义在一个函数内部的函数”。例如在 JavaScript 中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“能够读取其他函数内部变量的函数”

讲一下工作中哪些地方用到了闭包

闭包是一种特殊的函数,它可以读取其他函数内部的变量,同时让这些变量的值始终保持在内存中,不会在调用后被自动清除。闭包可以用在许多地方,其中最大的用处有两个:一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在调用后被自动清除1。

闭包在工作中有很多应用场景。比如,当我们需要将一个函数作为参数传递给另一个函数时,就可以使用闭包来实现。此外,在事件处理、回调函数、模块化开发等方面也有广泛应用

闭包的优缺点

闭包的优点有:

  1. 可以读取函数内部的变量,避免了全局污染。
  2. 可以让这些变量的值始终保持在内存中,不会在函数调用后被自动清除。
  3. 可以方便调用上下文的局部变量。
  4. 可以加强封装性,达到对变量的保护作用。

闭包的缺点有:

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
  2. 闭包可能会导致代码阅读困难。

如何避免闭包导致的内存泄露?

闭包是指函数内部定义的函数,该函数可以访问外部函数的变量,即使外部函数已经返回。闭包可以帮助我们在JavaScript中实现模块化编程,但是如果不小心使用,它们可能会导致内存泄漏。为了避免闭包导致的内存泄漏,可以在退出函数之前将不再使用的局部变量全部删除。例如,可以将变量赋值为null。

如果您使用的是ES6或更高版本,则可以使用let或const关键字而不是var关键字来声明变量。这样做可以避免变量被意外地提升到全局作用域中,并且在退出函数时自动删除变量。

如果您使用的是Node.js,则可以使用Node.js的内存分析工具来检测内存泄漏。这个工具可以帮助您找到哪些对象正在占用内存,并且可以帮助您找到哪些对象正在阻止垃圾回收器回收内存。

使用闭包实现每秒打印1,2,3,4

function printNum() {
  var num = 1;
  var timer = setInterval(function() {
    console.log(num);
    if (num === 4) {
      clearInterval(timer);
    }
    num++;
  }, 1000);
}
printNum();

数组有哪些方法

  • concat():连接两个或更多的数组,并返回结果。
  • copyWithin():从数组的指定位置拷贝元素到数组的另一个指定位置中。
  • entries():返回数组的可迭代对象,包含每个索引的键/值对。
  • every():检测数组元素是否都符合指定条件。
  • fill():使用一个固定值来填充数组。
  • filter():检测并返回符合条件的所有元素的数组。
  • find():返回符合条件的第一个元素。
  • findIndex():返回符合条件的第一个元素索引。
  • flat():将嵌套数组变成一维数组。
  • flatMap():对数组中每个元素执行函数,并将结果压缩成一维数组。
  • forEach():调用数组中的每个元素,并将元素传递给回调函数。
  • includes():检测数组是否包含某个元素。
  • indexOf():搜索数组中某个元素并返回其位置。
  • join():把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。
  • keys():返回数组的可迭代对象,包含每个索引的键。
  • lastIndexOf():搜索数组中某个元素并返回其位置(从后往前)。
  • map():对数组中每个元素执行函数,并返回新数组。
  • pop():删除并返回数组的最后一个元素。
  • push():向数组的末尾添加一个或更多元素,并返回新的长度。
  • reduce():将数组元素计算为单个值(从左到右)。
  • reduceRight():将数组元素计算为单个值(从右到左)。
  • reverse():颠倒数组中元素的顺序。
  • shift():删除并返回数组的第一个元素。
  • slice():从某个已有的数组返回选定的元素。
  • some():检测是否至少有一个数组元素符合指定条件。
  • sort():对数组的元素进行排序。
  • splice():删除元素,并向数组添加新元素。
  • toLocaleString():把数组转换为本地字符串,并返回结果。
  • toString():把数组转换为字符串,并返回结果。
  • unshift():向数组的开头添加一个或更多元素,并返回新的长度。
  • values():返回数组的可迭代对象,包含每个索引的值。

详细介绍看MDN

js 类数组的定义、类数组如何转换为数组

类数组是一种类似数组的对象,但是不能直接使用数组的方法。类数组对象有一个 length 属性,但是没有其他数组属性,例如 push 和 forEach 等。类数组对象可以通过 Array.from() 或者 Array.prototype.slice.call() 方法转换为真正的数组。例如:

let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 };
let arr1 = Array.prototype.slice.call(arrayLike);
let arr2 = [].slice.call(arrayLike);

以上两种方法都可以将类数组对象转换为真正的数组。其中,Array.from() 方法可以将可迭代对象和类数组对象转换为真正的数组。例如:

let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 };
let arr = Array.from(arrayLike);

继承的几种方式,如何的实现

JavaScript 中实现继承的六种方式包括:

  • 原型链继承:通过将父类的实例作为子类的原型来实现继承。
  • 借用构造函数继承:在子类构造函数中调用父类构造函数,使用 call 或 apply 方法。
  • 原型式继承:通过 Object.create() 方法创建一个新对象,将父对象作为新对象的原型。
  • 组合继承:使用原型链和借用构造函数的技术组合实现继承。
  • 寄生组合式继承:在组合继承的基础上,使用 Object.create() 方法来优化。
  • ES6中class继承:class可以通过extends关键字实现继承,不过class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

详细说明可参考:JavaScript怎样实现继承?js常见的六种继承方式-js教程-PHP中文网

箭头函数与普通函数的区别

  • 语法更加简洁、清晰。
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。
  • 箭头函数都是匿名函数,而普通函数可以是匿名函数,也可以是具体名函数。
  • 箭头函数不能用于构造函数,不能使用new,而普通函数可以用于构造函数,以此创建对象实例。

this的理解

在 JavaScript 中,this 是一个关键字,它指向当前函数的执行环境。具体来说,this 的值取决于函数的调用方式。在全局作用域中,this 指向全局对象。在函数内部,this 指向调用该函数的对象。如果函数不是作为对象的方法调用的,则 this 指向全局对象。如果使用 new 关键字调用函数,则 this 指向新创建的对象。

call、apply、bind的区别

在 JavaScript 中,call、apply 和 bind 都是用来改变函数中 this 的指向的方法。它们的区别在于传参的方式和返回值的不同。call 和 apply 的作用是一样的,都是改变函数中 this 的指向,只是传参的方式不同。call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面。apply 的所有参数都必须放在一个数组里面传进去。bind 方法和 call 方法很相似,也是改变函数中 this 的指向,但它与 call 和 apply 不同之处在于它会返回一个新函数,而不是立即执行原函数。

函数柯理化

函数柯里化是一种函数式编程技术,它可以把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数。这个技术以逻辑学家 Haskell Curry 命名的。

函数柯里化的作用和特点有三个:参数复用、提前返回、延迟执行。

  • 参数复用是指将多个参数的函数转化为接受单一参数的函数,这样可以减少代码量,提高代码的可读性和可维护性。
  • 提前返回是指将一个多参数函数转化为一个嵌套的一元函数序列,每个函数都返回一个新函数,直到最后一个函数返回最终结果。这样可以在需要时提前返回部分结果,避免不必要的计算。
  • 延迟执行是指将一个多参数函数转化为一个嵌套的一元函数序列,每个函数都返回一个新函数,直到最后一个函数被调用时才执行计算。这样可以避免不必要的计算,提高程序的性能。

下面是一个简单的例子,它将两个数字相加:

function add(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = add(5);
console.log(add5(3)); // 8

在这个例子中,add函数接受一个数字x并返回一个新函数,该新函数接受另一个数字y并返回它们的和。然后,我们使用add(5)来创建一个新函数add5,该函数将5添加到其输入中。最后,我们使用add5(3)来计算5 + 3 = 8。

深浅拷贝

深浅拷贝是计算机科学中的一个概念,用于描述在计算机程序设计中对象复制的方式。浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存。深拷贝则是完全拷贝了父对象及其子对象,新旧对象不共享内存。

JavaScript中实现深拷贝的方法有很多,其中一种是使用JSON对象的parse和stringify方法来实现深复制。这种方法将对象转换成字符串,然后再将其重新解析为新的对象,因此需要注意的是,它无法复制函数和某些JavaScript特有的对象类型(如Date、RegExp、Error、Function等)。

另外,还可以使用第三方库如Lodash或jQuery等来实现深拷贝。如果你想自己实现深拷贝,可以考虑使用递归或迭代的方式来遍历对象并复制。

浅拷贝则是将一个对象的引用赋值给另一个对象,两个对象会共享同一个内存地址,因此修改其中一个对象会影响到另一个对象。

JavaScript事件循环(Event Loop)是什么?

JavaScript事件循环(Event Loop)机制是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。它是让 JavaScript 做到既是单线程,又绝对不会阻塞的核心机制,也是 JavaScript 并发模型(Concurrency Model)的基础。

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务3。

详细内容参考JavaScript事件循环机制(event loop)

浏览器和 Node.js 的事件循环机制有什么区别?

浏览器和 Node.js 的事件循环机制有所不同。在浏览器中,JavaScript 的事件循环机制是根据 HTML5 定义的规范来实现的,而在 Node.js 中,事件循环是基于 libuv 库实现的。其中一个主要的区别在于浏览器的 event loop 和 Node.js 的 event loop 在处理异步事件的顺序是不同的。Node.js 中有 micro event,其中 Promise 属于 micro event。该异步事件的处理顺序就和浏览器不同。Node.js V11.0 以上这两者之间的顺序就相同了。

事件冒泡

事件冒泡是指在DOM事件流中,事件从最内层的元素开始发生,然后逐级向上传播到最外层。当一个元素接收到事件的时候,会把它接收到的事件传给自己的父级,一直到window。这个过程就是事件冒泡。

在JavaScript中,可以使用event.stopPropagation()来阻止事件冒泡。这个方法会阻止事件从被点击的元素向上冒泡到父元素。例如,如果你有一个按钮和一个包含该按钮的div,当你点击按钮时,事件会首先在按钮上触发,然后向上冒泡到div。如果你想阻止事件冒泡到div,你可以在按钮的事件处理程序中使用event.stopPropagation()。这样,当你点击按钮时,只有按钮的事件处理程序会被调用,而不是div的事件处理程序。

在React中,可以使用e.stopPropagation()来阻止合成事件之间的冒泡,但无法阻止合成事件到原生事件的冒泡。在Vue中,可以使用@click.stop来阻止事件冒泡。

事件捕获

事件捕获是由微软公司提出的,事件从文档根节点(Document 对象)流向目标节点,途中会经过目标节点的各个父级节点,并在这些节点上触发捕获事件,直至到达事件的目标节点。在事件捕获阶段,事件会从 DOM 树的最外层开始,依次经过目标节点的各个父节点,并触发父节点上的事件,直至到达事件的目标节点。

事件委托

事件委托是一种利用事件冒泡的机制,将事件处理器绑定到一个父元素上,以代理子元素上的事件。这样可以减少事件处理器的数量,提高性能,同时也可以避免由于动态添加或删除子元素而导致的事件处理器失效的问题。

js 延迟加载的方式ajax是什么?

  • defer 属性:将 defer 属性设置为 true,浏览器会在 HTML 解析完成后再加载脚本,并且在 DOMContentLoaded 事件触发前执行脚本,从而避免了阻塞页面渲染。
  • async 属性:将 async 属性设置为 true,浏览器会在 HTML 解析完成后立即加载脚本,但是不会等待脚本下载和执行完成,而是继续解析 HTML 文档。
  • 动态创建 DOM 方式:使用 JavaScript 动态创建 script 标签,然后将其添加到文档中。这种方式可以让脚本在页面加载完毕后再加载。
  • 使用 jQuery 的 getScript 方法:该方法可以异步加载 JavaScript 文件并执行回调函数。
  • 使用 setTimeout 延迟方法:使用 setTimeout 延迟方法可以让脚本在页面加载完毕后再加载。
  • 让 js 最后加载:将 script 标签放在 body 标签的最后面,这样可以让页面先渲染出来,然后再去加载 js 文件。

同步任务与异步任务

JavaScript是一种单线程的编程语言,同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。

在JavaScript中,同步任务在主线程中执行,形成一个执行栈。异步任务通过回调函数实现,把任务添加到任务队列中。当主线程中的同步任务全部执行完毕后,系统就会读取"任务队列"中的异步任务。

宏任务与微任务

在JavaScript中,宏任务和微任务是异步任务的两种类型。宏任务包括:script、setTimeout、setInterval、I/O、UI render、postMessage、MessageChannel、setImmediate (Node.js 环境)。微任务包括:Promise.then、Object.observe、MutationObserver、process.nextTick (Node.js 环境)。

当执行栈为空时,事件循环会从宏任务队列中取出一个任务执行。在执行宏任务的过程中,如果遇到了微任务,就会将微任务添加到微任务队列中。当宏任务执行完毕后,会立即执行当前微任务队列中的所有微任务。

模块规范(commonJS、es6 、AMD、CMD)

这些规范都是用于在模块化定义中使用的。CommonJS、AMD、CMD是ES5中提供的模块化编程的方案,而import/export是ES6中定义新增的。

CommonJS主要针对服务端,AMD/CMD/ES Module主要针对浏览器端。

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。

AMD和CMD都是异步加载模块,区别在于AMD推崇依赖前置,提前执行,CMD推崇依赖就近,延迟执行。

ES6 Module是ECMAScript 6标准中新增的模块化规范,它与CommonJS和AMD/CMD不同之处在于它是静态的,也就是说,在代码运行之前就可以确定模块的依赖关系。

js 的垃圾回收 与 v8 的垃圾回收

JavaScript 的垃圾回收机制是指在运行时自动回收不再需要使用的对象内存,也即是垃圾回收。V8 引擎是目前主流的 JavaScript 引擎之一,它在运行时自动回收不再需要使用的对象内存,也即是垃圾回收。V8 使用了全暂停式(stop-the-world)、分代式(generational)、精确(accurate)等组合的垃圾回收机制,来确保更快的对象内存分配、更短的垃圾回收时触发的暂停以及没有内存碎片。

相比于 JavaScript 的垃圾回收机制,V8 引擎的垃圾回收机制更加复杂。V8 引擎将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。

哪些操作会造成内存泄漏

内存泄漏是指一些对象我们不再使用它的时候,它仍然存在。下面是一些可能会造成内存泄漏的操作:

  • 意外的全局变量引起的内存泄漏
  • 闭包引起的内存泄漏
  • 没有清理的DOM元素引用
  • 循环引用
  • setTimeout 的第一个参数使用字符串而非函数

0.1 + 0.2 != 0.3

这是因为在 JavaScript 中,0.1 和 0.2 都不能被精确地表示为二进制小数,所以它们的和也不能被精确地表示为二进制小数。这是由于 JavaScript 使用 IEEE 754 标准来表示数字,该标准使用二进制小数来表示数字。在这种情况下,0.1 和 0.2 都不能被精确地表示为二进制小数,因此它们的和也不能被精确地表示为二进制小数。

更多内容可参考:聊聊为啥在js里0.1+0.2不等于0.3

如何解决图片懒加载与预加载

图片懒加载和预加载都是前端优化的手段,可以提高页面的加载速度和用户体验。图片懒加载是指在页面加载时,只加载当前可视区域内的图片,而不是所有图片,当用户滚动到未加载的图片时再进行加载。这样可以减少页面的请求次数,提高页面的加载速度。而图片预加载则是在页面加载时,提前将所有图片进行加载,这样可以保证用户在查看时能够快速、无缝地浏览图片。但是预加载会增加页面的请求次数和带宽占用,因此需要根据具体情况进行选择。

图片预加载是指在页面加载时,提前加载图片资源,以提高用户体验。实现图片预加载的方法有很多,其中一种方法是使用JavaScript创建Image对象,然后绑定Image对象的src属性到图片路径,让其实现加载,这样图片就会加载到浏览器缓存,实现图片的预加载效果。

图片懒加载是一种优化网页性能的技术,可以减少页面的加载时间,提高用户体验。实现图片懒加载的方法有很多,其中一种方法是通过自定义属性如【data-imgurl】,存放着图片的路径;然后通过js判断界面滚动的位置或图片是否已加载;最后加载再去获取属性【data-imgurl】的值赋给src即可。

另外,还有两种方式可实现:

  1. 监听 onscroll 事件,通过对每一个节点添加 getBoundingClientRect 方法;
  2. 通过 HTML5 的 IntersectionObserver (交叉观察器) 配合监听元素的 isIntersecting 属性。

JavaScript 中数组是如何存储的?

在 JavaScript 中,数组不是以一段连续的区域存储在内存中,而是一种哈希映射的形式,它可以通过多种数据结构实现,其中一种是链表。JavaScript 数组是可调整大小的,并且可以包含不同的数据类型。 但是,JavaScript 数组不是关联数组,因此不能使用任意字符串作为索引访问数组元素,必须使用非负整数(或它们各自的字符串形式)作为索引访问。

JavaScript 中的数组为什么可以不需要分配固定的内存空间?

JavaScript 中的数组不需要分配固定的内存空间,因为数组的空间是连续的,这就意味着在内存中会有一整块空间来存放数组,如果不是固定长度,那么内存中位于数组之后的区域会没办法分配,内存不知道数组还要不要继续存放,要使用多长的空间。此外,JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。

Iterator和for…of

Symbol.iterator是ES6中的一个内置Symbol,它是一个迭代器接口,只有对象里有这个symbol的属性,才可以认为此对象是可迭代的。凡是具有Symbol.iterator属性的数据结构都可以被for…of 循环调用,我们可以手动的给对象添加 Symbol.iterator 属性。一个对象如果要具备可被 for…of 循环调用的 Iterator 接口,就必须在 Symbol.iterator 的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

promise的实现

Promise 是 JavaScript 中的一个异步编程解决方案。Promise 的实现原理是基于回调函数,只不过是把回调封装在了内部,使用上一直通过 then 方法的链式调用,使得多层的回调嵌套看起来变成了同一层的,书写上以及理解上会更直观和简洁一些。Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,所以我们称之为起始函数。起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。当 Promise 状态发生改变时,就会调用 then 方法注册的回调函数。

详细实现可参考从如何使用到如何实现一个Promise

promise的各种api

  • Promise.all(iterable): 返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
  • Promise.race(iterable): 返回一个 Promise 实例,此实例在 iterable 参数内任意一个 promise“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
  • Promise.reject(reason): 返回一个状态为失败的 Promise 实例,失败原因为 reason。
  • Promise.resolve(value): 返回一个状态由给定 value 决定的 Promise 实例。如果该值是 thenable(即带有then方法),返回的 Promise 实例会“跟随”这个 thenable 的对象,采用它的最终状态;否则返回的 Promise 实例将以该值完成(resolve)。此函数将类 promise 对象转换为真正的 Promise 对象。
  • Promise.prototype.catch(onRejected): 指定发生错误时的回调函数。catch方法返回一个新的Promise实例,该实例状态为rejected。
  • Promise.prototype.then(onFulfilled, onRejected): 指定当Promise状态为fulfilled时的回调函数。then方法返回一个新的Promise实例,该实例状态由onFulfilled函数执行结果决定。
  • Promise.prototype.finally(onFinally): 指定不管 Promise 对象最后状态如何,都会执行的操作。finally方法返回一个新的Promise实例,该实例状态由原Promise实例的状态决定。
  • Promise.allSettled(iterable): 返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调完成(resolve),返回值是一个对象数组,每个对象表示对应的 promise 结果。
  • Promise.any(iterable): 返回一个 Promise 实例,此实例在 iterable 参数内任意一个 promise“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 全部失败(rejected),此实例回调失败(reject),失败原因是所有失败 promise 的结果组成的数组。
  • Promise.try(fn): 将fn函数封装成promise对象并执行。

async / await

async/await是一种异步编程的方式,它是基于Promise的语法糖。async/await让异步代码看起来像同步代码,使得代码更加易读易懂。async/await是ES2017引入的新特性,它可以让我们更加方便地处理异步操作。

async/await是通过async函数和await表达式来实现的。async函数是一个返回Promise对象的异步函数,而await表达式可以暂停async函数的执行,等待Promise对象的状态发生改变后再继续执行async函数。

什么是Proxy

Proxy是ES6新增的一个构造函数,用于生成一个对象的代理对象,即可以拦截并重定义对象的底层操作,比如读取属性、设置属性、函数调用等。Proxy可以用来实现元编程,即编写代码来操作代码本身。

Proxy 的使用场景很多,以下是一些常见的使用场景:

  1. 抽离校验模块:使用 Proxy 可以保障数据类型的准确性。
  2. 私有属性:使用 Proxy 可以实现私有属性。
  3. 访问日志:使用 Proxy 可以记录属性或接口的使用情况或性能表现。
  4. 预警和拦截:使用 Proxy 可以实现预警和拦截功能。
  5. 过滤操作:使用 Proxy 可以过滤一些操作。
  6. 中断代理:使用 Proxy 可以中断代理。

Proxy 的优点如下:

  1. 可以直接监听对象而非属性;
  2. 可以直接监听数组的变化;
  3. 有多种(13种)拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等,是 Object.defineProperty 不具备的;
  4. 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  5. Proxy 可以代理一组对象,并统一处理它们的操作。

Proxy 的缺点如下:

  1. 兼容性问题:Proxy 是 ES6 中新增的特性,因此在一些老版本浏览器中不支持;
  2. 性能问题:Proxy 比 Object.defineProperty 慢了很多,因此在一些对性能要求比较高的场景中不适用。

Generator及其异步方面的应用说一说

Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。Generator函数有多种理解角度。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象。

在异步方面的应用中,Generator函数可以通过yield表达式暂停执行,通过next方法恢复执行。这个特点使得Generator函数可以不需要回调函数完成异步操作。在异步操作中,我们可以使用Promise对象来管理异步操作的状态,并使用Generator函数来简化异步操作的流程。

需要注意的是,在使用Generator函数进行异步编程时,我们需要定义我们自己的 runGenerator (…) 工具来实现generator+promises模式。