ES6
ES6 语法知道哪些,分别怎么用?
- let
- 声明的变量只在 let 命令所在的代码块有效
- 没有变量提升,变量一定要在声明后使用(暂时性死区)
- let 不允许在相同作用域内重复声明同一个变量
- const
- 与 let 基本相同
- const 声明一个只读常量,一旦声明,值不可更改
- 本质上不是变量的值不可改动,而是变量指向的那个内存地址不可改动
- 对于对象和数组,变量指向的内存地址保存的只是一个指针,const 只能保证指针不变,指针指向的数据结构无法控制
- 如果要冻结对象,应该使用
Object.freeze方法
- 解构赋值
- 按照一定模式从数组和对象中提取值,然后对变量进行赋值
- 数组的解构赋值,变量的取值是由它的位置决定的
- 对象的解构赋值,变量必须与属性同名
- 箭头函数
- this 指向的固定化:实际原因是因为箭头函数根本没有自己的 this,导致内部的 this 就是外层代码块的 this。
- 扩展运算符(...)
将一个数组专为用逗号分隔的参数序列。
- 替代数组的 apply 方法
- 合并数组
- 与解构赋值结合,用于生成数组,但注意只能放在参数最后一位
- Symbol
一种新的原始数据类型,类似于字符串,表示独一无二的值。
- Symbol 函数的参数只表示对当前 Symbol 值的描述,就算相同参数的 Symbol 函数返回值也不相同
- 在对象内部,使用 Symbol 值定义属性时必须放在方括号内,因为点运算符后面总是字符串
- 要使用
Object.getOwnPropertySymbols方法获取指定对象的所有 Symbol 属性名,方法返回一个数组
- Set 和 Map 数据结构
- Set:类似数组,成员都是唯一的,没有重复
const s = new Set()- Set.prototype.size 属性返回实例的成员总数
- add(value)
- delete(value)
- has(value)
- clear()
- keys()
- values()
- entries()
- forEach()
- Map:类似对象,也是键值对的集合,但是键可以是各种类型的值(包括对象)
- size 属性
- set(key, value)
- get(key)
- has(key)
- delete(key)
- clear()
- keys()
- values()
- entries()
- forEach()
- Set:类似数组,成员都是唯一的,没有重复
- Promise 详见下一题。
- Class
Class 作为构造函数的语法糖,可以通过 extends 关键字实现继承。
- 子类必须在 constructor 方法中调用 super 方法 因为子类没有自己的 this 对象,需要继承父类的 this 对象,不调用 super 方法子类就得不到 this 对象。
Object.getPrototypeOf()用于从子类上获取父类。- super 关键字既可当作函数使用,也可当作对象使用
- super():作为函数调用时代表父类的构造函数。只可在子类的构造函数中,否则会报错。
- super 对象:在普通方法中指向父类的原型对象(所以定义在父类实例上的方法或属性无法获取)。通过 super 调用父类方法时,super 会绑定子类的 this。
- Generator 函数
function 命令与函数名之间有一个星号,且函数体内部使用 yield 语句定义不同的内部状态。
调用 Generator 函数后,该函数并不执行,返回的不是运行结果,是一个指向内部状态的指针对象。 必须调用 next 方法,使指针移向下一个状态,直到遇到下一条 yield 语句或 return 语句。如果 next 方法有参数,那这个参数会作为上个阶段的返回结果。function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; }let hw = helloWorldGenerator(); hw.next() //{ value: 'hello', done: false } hw.next() //{ value: 'world', done: false } hw.next() //{ value: 'ending', done: ture } hw.next() //{ value: undefined, done: ture } - async 函数
是 Generator 的语法糖。形式上就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。相对于 Generator 函数的改进体现为:
- 内置执行器:只要调用了 async 函数就会自动执行,输出最后结果
- 更好的语义:async 表示有异步操作,await 表示后面的表达式需要等待结果
- 适用性更广:await 命令后面可以是 Promise 对象(异步)和原始类型的值(同步)
- 返回值是 Promise:async 函数可以看作由多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖 async 函数返回的 Promise 对象必须等到内部所有 await 命令后面的 Promise 对象执行完才会发生状态改变,除非遇到 return 语句或抛出错误。就是说只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。
Promise、Promise.all、Promise.race 分别怎么用?
Promise 是一个对象,可以获取异步操作的消息。 解决了回调地狱的问题(多个回调函数嵌套)。
- Promise 对象是一个构造函数,用来生成 Promise 实例
new Promise()。构造函数接受一个函数作为参数,该函数的两个参数也是函数,分别是 resolve 和 reject:- resolve 函数:将 Promise 对象的状态从 Pending(未完成) 变为 Resolved(成功),在异步操作成功时调用,并将异步操作的结果作为参数传递出去
- reject 函数:将 Promise 对象的状态从 Pending(未完成) 变为 Rejected(失败),在异步操作失败时调用,并将异步操作报出的错误作为参数传递出去
- then 方法:第一个参数是 Resolved 状态的回调函数,第二个参数(不建议使用,建议使用catch)是 Rejected 状态的回调函数。then 方法返回一个新的 Promise 实例,所以可以使用链式写法
- catch 方法:用于指定发生错误时的回调函数。Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止
- Promise.all():用于将多个 Promise 实例包装成一个新的 Promise 实例
let p = Promise.all([p1, p2, p3]);- 接受一个数组作为参数,每个数组成员都是 Promise 实例,如果不是就先调用 Promise.resolve 方法将参数转为 Promise 实例
- 状态由参数所有的 Promise 实例决定:
- 只有都变成 Resolved,p 的状态才会变成 Resolved
- 只要有一个被 Rejected,那 p 的状态就会变成 Rejected,第一个被 Rejected 的实例的返回值会传递给 p 的回调函数
- 如果参数的 Promise 实例自身定义了 catch 方法,那它被 rejected 时不会出发 Promise.all()的 catch 方法
- Promise.race():用于将多个 Promise 实例包装成一个新的 Promise 实例
let p = Promise.race([p1, p2, p3]);- 与 Promise.all() 相同,接受一个数组作为参数,每个数组成员都是 Promise 实例,如果不是就先调用 Promise.resolve 方法将参数转为 Promise 实例
- 只要参数的 Promise 实例中有一个率先改变状态,那 p 的状态就跟着改变,率先改变的 Promise 实例的返回值就会传递给 p 的回调函数
前端异步都有哪些方式?
- 回调函数(Callbacks)
- 定义:将一个函数作为参数传递给另一个函数,在特定的操作完成后调用这个回调函数。
- 示例:比如使用 XMLHttpRequest 进行异步请求时,可以通过设置回调函数来处理响应。
const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://example.com/api/data'); xhr.onload = function() { if (xhr.status === 200) { // 处理响应数据 console.log(xhr.responseText); } }; xhr.send();
- Promise
- 定义:Promise 是一种用于处理异步操作的对象,它代表了一个尚未完成但预期在未来会完成的操作,并提供了一种方式来处理操作成功或失败的结果。
- 示例:例如使用 fetch API 进行网络请求,它返回一个 Promise。
fetch('https://example.com/api/data') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
- async/await
- 定义:这是 ES2017 引入的语法糖,基于 Promise 实现,使异步代码看起来更像同步代码,提高了代码的可读性和可维护性。
- 示例:
- setTimeout
async/await 和 Promise 的区别
- 语法和可读性
- Promise:使用
.then()和.catch()方法来处理异步操作的成功和失败情况。这种方式在处理多个异步操作的复杂逻辑时可能会导致回调嵌套,使得代码难以阅读和维护。 - async/await:是一种基于 Promise 的语法糖,使得异步代码看起来更像同步代码,提高了代码的可读性和可维护性。使用 try/catch 块来处理异步操作中的错误,更加直观。
- Promise:使用
- 错误处理
- Promise:
- 通过
.catch()方法捕获异步操作中的错误。 - 如果在多个
.then()链中发生错误,需要在每个.then()中返回一个被拒绝的 Promise 或者使用.catch()来捕获错误,否则错误可能会被忽略。
- 通过
- async/await:
- 使用 try/catch 块集中处理异步操作中的错误,更加简洁和直观。所有的错误都可以在 catch 块中处理。
- Promise:
- 代码结构和流程控制
- Promise:
- 可以通过
.then()方法的链式调用和.all()、.race()等静态方法来组合多个异步操作,但代码的流程控制相对复杂。 - 例如使用
Promise.all()来同时执行多个异步操作并等待它们全部完成。
- 可以通过
- async/await:
- 可以使用 await 关键字在异步函数中等待一个 Promise 的解决,使得代码的流程控制更加自然和直观。
- 例如可以在一个 async 函数中依次等待多个异步操作的完成,类似于同步代码的执行顺序。
- Promise:
script 标签中 defer 和 async 的区别?
defer 和 async 属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面HTML的解析,其区别如下:
-
执行顺序 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行。
-
脚本是否并行执行 async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行进行的,即异步执行;defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded事件触发执行之前。
-
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,之前加载到一半的HTML页面会停止下来,被阻塞加载。
-
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行,将script变成异步,当script异步解析完成后,如果HTML页面还没有完成解析,又会继续阻塞页面的解析。
-
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行,将script变成异步。但是 script.js 的执行要在所有元素解析完成之后,类似于将这个script放在了页面的底部。
总结:async和defer都是异步加载js。不同的是async是js只要一加载完毕就会马上执行,不管html有没有解析完毕。所以它有可能阻塞html解析。而defer要等到html解析完毕之后才执行。所以不会阻塞html解析。
箭头函数和普通函数的区别?
- 箭头函数都是匿名函数
- 箭头函数本身无this,其this指向父级作用域的this,并且call、bind、apply无法改变this的指向
- 不可以被当做构造函数:由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用
- 箭头函数比普通函数更加简洁,箭头函数如果没有参数,直接写一个空括号即可。参数只有一个,也可以省去包裹参数的括号
- 箭头函数不绑定arguments,取而代之用rest参数代替arguments对象
- 箭头函数没有prototype,普通函数有
ES6中对象新增的方法有哪些?
Object.assign()方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象(target)。Object.is它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。值和对象类型的值都可以,NAN这种特殊值也可以处理。Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。Object.keys方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
class 和 function 的区别
-
相同点:都可作为构造函数,通过
new操作符来实例化- 函数作为构造函数,通常首字母要大写。构造函数中的this指向构造函数创建出来的实例对象。
function Person(name) { this.name = name; } const usr = new Person('Jack'); - 类实现构造函数,可以包含构造函数方法、实例方法、setter函数、getter函数和静态类方法,但这些都不是必须的。用new操作符创建类的实例时,会自动调用这个constructor函数。若不定义constructor,相当于constructor为空函数。
class Person { constructor(name) { this.name = name; } } const usr = new Person('Jack');
- 函数作为构造函数,通常首字母要大写。构造函数中的this指向构造函数创建出来的实例对象。
-
不同点
- class构造函数必须使用new操作符 普通function构造函数如果不使用new调用,那么会以全局的this(在浏览器中是window)作为内部对象。
- class声明不可以提升 function构造函数声明存在提升,也就是定义构造函数的部分可以写在实例化对象的后面;class声明不能。
- class不可以用call、apply、bind改变执行上下文
Class 类
Class 作为构造函数的语法糖,可以通过 extends 关键字实现继承。
- 子类必须在 constructor 方法中调用 super 方法 因为子类没有自己的 this 对象,需要继承父类的 this 对象,不调用 super 方法子类就得不到 this 对象。
Object.getPrototypeOf()用于从子类上获取父类。- super 关键字既可当作函数使用,也可当作对象使用
- super():作为函数调用时代表父类的构造函数。只可在子类的构造函数中,否则会报错。
- super 对象:在普通方法中指向父类的原型对象(所以定义在父类实例上的方法或属性无法获取)。通过 super 调用父类方法时,super 会绑定子类的 this。
Function 定义的构造函数的继承
- 定义父类构造函数
function Parent(name) {
this.name = name
this.sayHello = function () {
console.log(`Hello, I am ${this.name}.`)
}
}
- 定义子类构造函数
function Child(name, age) {
Parent.call(this, name) // 调用父类构造函数,继承父类的属性
this.age = age
}
- 实现继承
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
- 使用子类构造函数创建对象
const child = new Child("Tom", 10)
child.sayHello() // 继承自父类的方法
console.log(child.age) // 子类特有的属性
Class 使用 extends 继承的原理
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
let child = new Child('John', 10);
child.sayName(); // 输出 'John'
child.sayAge(); // 输出 10
底层原理基于原型链和构造函数调用:
- 原型链的构建
- 当使用 extends 关键字时,
Child.prototype的原型会被设置为Parent.prototype。这意味着 Child 类的实例可以访问 Parent 类原型上定义的方法。从本质上讲,这是在构建原型链,就像传统的原型链继承一样。例如,在上面的代码中,child 实例可以访问 sayName 方法,因为Child.prototype.__proto__(隐式原型)指向了Parent.prototype。
- 当使用 extends 关键字时,
- 构造函数调用(super 关键字)
- super 关键字在 Child 类的构造函数中有重要作用。它主要用于调用父类(Parent)的构造函数。
- 在 Child 类的构造函数中,
super(name)这一行会调用 Parent 类的构造函数,并传递 name 参数。这使得 Parent 类的构造函数可以正确地初始化继承过来的属性(如this.name)。 - 从底层实现角度看,super 的调用确保了父类的构造函数在子类构造函数中的正确执行,同时也建立了正确的 this 绑定关系。如果没有 super 调用,在子类构造函数中访问 this 之前,JavaScript 引擎会抛出错误。
与传统继承方式的比较:
- 对比原型链继承:传统的原型链继承是通过将子类的原型设置为父类的实例来实现的。而 class 继承通过 extends 和 super 更加直观和语义化。class 继承自动设置了正确的原型链关系,并且在构造函数中通过 super 明确了父类构造函数的调用,避免了传统原型链继承可能出现的一些问题,如忘记设置正确的原型或者在子类型实例中无法正确初始化父类型属性等。
- 对比组合继承:组合继承是结合了原型链继承和构造函数继承的方式。class 继承可以看作是一种更优雅的组合继承实现。它通过 extends 隐式地设置了原型链继承部分,通过 super 在构造函数中实现了类似构造函数继承的功能,而且避免了组合继承中两次调用父类构造函数(一次是在设置子类原型为父类实例时,另一次是在子类构造函数内部通过 call 或 apply 调用)导致的性能和属性重复初始化问题。
Symbol和BigInt
-
Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。 应用场景:- 对象中保证不同的属性名。(注意:使用
Symbol值定义属性的时候,必须放在方括号中) - 读取的时候也是不能使用点运算符
- 定义一组常量,保证这组常量都是不相等的
- 使用
Symbol定义类的私有属性/方法 Vue中的project和inject
- 对象中保证不同的属性名。(注意:使用
-
BigInt是一种数字类型的数据,它可以表示任意精度格式的整数,使用BigInt可以安全地存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围。
Symbol 的应用
-
在企业开发中如果需要对一些第三方的插件、框架进行自定义的时候 可能会因为添加了同名的属性或者方法, 将框架中原有的属性或者方法覆盖掉 为了避免这种情况的发生, 框架的作者或者我们就可以使用Symbol作为属性或者方法的名称
-
为对象定义一些非私有的、只用于内部的方法 对象的遍历方法:
for (let xx in obj)for (let xx of obj)Object.keys(obj):返回包含key的数组Object.values(obj):返回包含value的数组Object.getOwnPropertyNames():返回包含key的数组
上述的所有方法都是遍历不到symbol类型的,可以遍历到symbol的方法:
Object.getOwnPropertySymbols():返回对象中只包含symbol类型key的数组Reflect.ownKeys():返回对象中所有类型key的数组(包含symbol)
Map和Object的区别?
- 键的类型:
Map的键可以是任意数据类型(包括对象、函数、NaN等),而Object的键只能是字符串或者Symbol类型。 - 键值对的顺序:
Map中的键值对是按照插入的顺序存储的,而对象中的键值对则没有顺序。 - 键值对的遍例:
Map的键值对可以使用for...of进行遍历,而Object的键值对需要手动遍历键值对。 - 继承关系:
Map没有继承关系,而Object是所有对象的基类。
Map 和 WeakMap
-
Map
- Map默认情况下不包含任何键,所有键都是自己添加进去的。不同于Object原型链上有一写默认的键。
- Map的键可以时任何类型数据,就连函数都可以。
- Map的键值对个数可以轻易通过size属性获取,Object需要手动计算。
- Map在频繁增删键值对的场景下性能比Object更好。
-
WeakMap
- WeakMap只能将对象作为键名
- WeakMap的键名引用的对象是弱引用
-
Map 和 WeakMap 的区别
- Map 的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键
- Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;WeakMap 的键是弱引用,如果键不再有其他引用,垃圾回收机制可以自动回收键值对,此时键是无效的
- Map 可以被遍历,WeakMap 不能被遍历
强引用&弱引用:
- 强引用:当不再需要这某个对象时,我们必须手动删除它引用,解除对这个对象的引用关系,否则垃圾回收机制不会释放它占用的内存。
- 弱引用:指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收(引用计数不会计弱引用)。
ES6 模块和 CommonJS 模块有什么区别
- 语法不同
ES6 模块使用
import和export关键字来导入和导出模块,而 CommonJS 模块使用require和module.exports或exports来导入和导出模块
// ES6 模块
import { foo } from './module';
export const bar = 'bar';
// CommonJS 模块
const foo = require('./commonjs');
exports.bar = 'bar';
- 异步加载: ES6 模块支持动态导入(dynamic import),可以异步加载模块。这使得在需要时按需加载模块成为可能,从而提高了性能;CommonJS 模块在设计时没有考虑异步加载的需求,通常在模块的顶部进行同步加载
数组的 reduce 方法
数组的reduce()方法对数组中的每个元素执行一个传入的 reducer 函数(升序执行),将其结果汇总为单个返回值。
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
- callback:执行数组中每个值的函数,包含四个参数。
- accumulator:累计器,累计回调的返回值。它是上一次调用回调时返回的累积值,或者是提供的初始值(initialValue)。
- currentValue:数组中正在处理的当前元素。
- index(可选):当前元素在数组中的索引。
- array(可选):调用
reduce()的数组。
- initialValue(可选):作为第一次调用callback函数时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。
应用场景:
- 求和、求乘积等数值计算
const numbers1 = [1, 2, 3, 4]; const sum = numbers1.reduce((acc, cur) => acc + cur, 0); console.log(sum); // 10 const numbers2 = [2, 3, 4]; const product = numbers2.reduce((acc, cur) => acc * cur, 1); console.log(product); // 24 - 扁平化嵌套数组:可以将多维数组扁平化为一维数组:
const nestedArray = [[1, 2], [3, 4], [5, 6]]; const flatArray = nestedArray.reduce((acc, cur) => acc.concat(cur), []); console.log(flatArray); // [1, 2, 3, 4, 5, 6] - 对象属性值的累加,假设有一组对象表示商品及其数量,计算商品总数:
const items = [ { name: 'apple', count: 2 }, { name: 'banana', count: 3 }, { name: 'orange', count: 1 } ]; const totalCount = items.reduce((acc, curr) => acc + curr.count, 0); console.log(totalCount); // 6
JavaScript
JS 中使用 typeof 能得到哪些类型?
- boolean
- number
- string
- undefined
- symbol
- object
- function
- bigint:是一种内置对象,它提供了一种方法来表示大于 (2的53次方 - 1) 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()。
原始数据类型和引用数据类型在内存中的存储方式
- 栈 存放原始数据类型,栈中的简单数据段,占据空间小,属于被频繁使用的数据。
- 堆 存放引用数据类型,引用数据类型占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实 体的起始地址。
原型与原型链
- 原型
- JavaScript 的所有对象中都包含了一个
__proto__内部属性,这个属性所对应的就是该对象的原型 - JavaScript 的函数对象,除了原型
__proto__之外,还预置了 prototype 属性 - 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型
__proto__
- JavaScript 的所有对象中都包含了一个
- 原型链
- 任何一个实例对象通过原型链可以找到它对应的原型对象,原型对象上面的实例和方法都是实例所共享的
- 一个对象在查找一个方法或属性时,他会先在自己的对象上去找,找不到时,他会沿着原型链依次向上查找,直到找到该属性或方法,或者到达原型链的顶端(
Object.prototype的原型为 null) *注意:函数才有 prototype,实例对象只有__proto__,而函数有的__proto__是因为函数是Function的实例对象
null和undefined区别
undefined和null都是基本数据类型,这两个基本数据类型分别都只有一个值,就是undefined和null。
undefined代表的含义是未定义,一般变量声明了但还没有定义的时候会返回undefined,typeof为undefinednull代表的含义是空对象,null主要用于赋值给一些可能会返回对象的变量,作为初始化,typeof为object
instanceof运算符的实现原理
instanceof运算符适用于检测构造函数的prototype属性上是否出现在某个实例对象的原型链上。
instanceof运算符的原理是基于原型链的查找。当使用obj instanceof Constructor进行判断时,JavaScript 引擎会从obj的原型链上查找Constructor.prototype是否存在,如果存在则返回true,否则继续在原型链上查找。如果查找到原型链的顶端仍然没有找到,则返回false。
instanceof运算符只能用于检查某个对象是否是某个构造函数的实例,不能用于基本类型的检查。
typeof和instanceof区别
typeof与instanceof都是判断数据类型的方法,区别如下:
typeof会返回一个运算数的基本类型,instanceof返回的是布尔值instanceof可以准确判断引用数据类型,但是不能正确判断原始数据类型typeof虽然可以判断原始数据类型(null除外),但是无法判断引用数据类型(function除外)
为什么typeof判断null为object?
这是 JavaScript 语言的一个历史遗留问题,在第一版JS代码中用32位比特来存储值,通过值的1-3位来识别类型,前三位为000表示对象类型。而null是一个空值,二进制表示都为0,所以前三位也就是000,所以导致typeof null返回object。
判断数组的方式有哪些?
- 原型链
obj.__proto__ === Array.prototype;
Array.isArray()
Array.isArray(obj);
- 使用 instanceof 运算符
- 原理:instanceof 用于检查一个对象是否是某个构造函数的实例。对于数组来说,数组是由 Array 构造函数创建的,所以可以使用
arr instanceof Array来判断。但是,这种方法有一定的局限性,它在不同的 iframe 或 window 环境下可能会出现问题,因为每个环境都有自己独立的 Array 构造函数。 - 示例:
let arr = [1, 2, 3]; console.log(arr instanceof Array); // true
- 原理:instanceof 用于检查一个对象是否是某个构造函数的实例。对于数组来说,数组是由 Array 构造函数创建的,所以可以使用
- 使用
Object.prototype.toString.call()方法- 原理:
Object.prototype.toString()方法返回一个表示对象类型的字符串。当把这个方法通过 call 或 apply 绑定到一个数组对象上时,会返回[object Array]这样的字符串,通过比较这个字符串就可以判断是否是数组。这种方法比较通用,能够准确地判断各种情况下的数组,包括在不同的执行上下文或跨域场景下。 - 示例:
let arr = [1, 2, 3]; console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
- 原理:
对类数组对象的理解,如何转化为数组?
类数组也叫伪数组,类数组和数组类似,但不能调用数组方法,常见的类数组有arguments、通过document.getElements获取到的内容等,这些类数组具有length属性。
转换方法:
- 通过
call调用数组的slice方法来实现转换
Array.prototype.slice.call(arrayLike)
- 通过
call调用数组的splice方法来实现转换
Array.prototype.splice.call(arrayLike, 0)
- 通过
apply调用数组的concat方法来实现转换
Array.prototype.concat.apply([], arrayLike)
- 通过
Array.from方法来实现转换
Array.from(arrayLike)
Array.prototype.slice.call(arguments)调用的是arguments的slice方法,而typeof arguments = 'Object'而不是 Array,它没有slice这个方法,通过Array.prototype.slice.call调用,JavaScript 的内部机制应该是把arguments对象转化为Array。
深拷贝&浅拷贝的区别?
- 浅拷贝:如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
- 深拷贝:当对一个复杂对象(例如包含嵌套对象和数组的对象)进行拷贝时,如果只是简单地赋值,实际上只是创建了一个新的引用指向原对象,这种方式被称为浅拷贝。而深拷贝则会递归地复制对象及其所有嵌套的对象和数组,创建一个完全独立的副本,与原对象没有任何引用关系。实现深拷贝的方法有多种。比如:
- 可以使用
JSON.parse(JSON.stringify(obj))的方式,但这种方法有一些局限性,例如不能拷贝函数、循环引用等特殊对象。 - 可以通过递归遍历对象或数组的方式手动实现深拷贝,对于每个属性都判断其类型,如果是对象或数组则继续递归进行深拷贝。
- 可以使用
使用 JSON 序列化和反序列化来做深拷贝有什么缺点?
- 不支持函数和特殊对象:JSON 只支持有限的数据类型,如对象、数组、字符串、数字、布尔值和 null。如果对象中包含函数、正则表达式、日期对象、Symbol 等特殊类型,在序列化过程中这些特殊类型的数据会丢失信息。例如,一个包含方法的 JavaScript 对象,序列化后再反序列化,方法将会消失。
- 数据精度问题:对于某些高精度的数值类型,JSON 序列化和反序列化可能会导致精度损失。比如在 JavaScript 中,超出了Number类型安全整数范围(
-2^53到2^53 - 1)的大整数,在 JSON 处理过程中可能会出现精度丢失。 - 序列化和反序列化开销:这个过程涉及到对整个对象结构进行解析和重新构建,对于大型复杂对象,会消耗较多的时间和计算资源。
- 内存占用:在序列化时,需要在内存中创建 JSON 字符串,对于非常大的对象,这可能会占用大量的临时内存空间。
- 无法处理循环引用对象:如果对象中存在循环引用(例如,对象 A 包含指向对象 B 的引用,而对象 B 又包含指向对象 A 的引用),使用 JSON 序列化时会抛出错误。
object.assign和扩展运算法是深拷贝还是浅拷贝?两者区别
都是浅拷贝
Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6setter。 扩展操作符...使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制 ES6 的symbols属性。
new 操作符的实现原理
new操作符用来创建一个对象,并将该对象绑定到构造函数的this上。
new操作符的执行过程:
- 创建一个空对象
- 设置原型,将构造函数的原型指向空对象的
prototype属性 - 将
this指向这个对象,通过apply执行构造函数 - 判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,就返回这个引用类型的对象
for...in和for...of的区别
fo...of遍历获取的是对象的键值,for...in获取的是对象的键名for...in会遍历对象的整个原型链,性能非常差不推荐使用,而for...of只遍历当前对象不会遍历原型链- 对于数组的遍历,
for...in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of只返回数组的下标对应的属性值
总结:for...in循环主要是为了遍历对象而生,不适用于遍历数组;for...of循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
如何使用for...of遍历对象
-
为什么不能遍历对象
for...of是作为 ES6 新增的遍历方式,能被其遍历的数据内部都有一个遍历器 iterator 接口,而数组、字符串、Map、Set 内部已经实现,普通对象内部没有,所以在遍历的时候会报错。想要遍历对象,可以给对象添加一个Symbol.iterator属性,并指向一个迭代器即可 -
如何实现 在迭代器里面,通过
Object.keys获取对象所有的key,然后遍历返回key和value:
var obj = {
a:1,
b:2,
c:3
}
obj[Symbol.iterator] = function*(){
var keys = Object.keys(obj);
for(var k of keys){
yield [k,obj[k]]
}
}
for(var [k,v] of obj){
console.log(k,v)
}
对 AJAX 的理解,实现一个 AJAX 请求
AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建 AJAX 请求的步骤:
- 创建一个
XMLHttpRequest对象 - 在这个对象上使用
open方法创建一个HTTP请求,open方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息 - 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过
setRequestHeader方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有5个状态,当它的状态变化时会触发onreadystatechange事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的readyState变为4的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过response中的数据来对页面进行更新了 - 当对象的属性和监听函数设置完成后,最后调用
send方法来向服务器发起请求,可以传入参数作为发送的数据体
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
ajax、axios、fetch 的区别
-
ajax
- 基于原生 XHR 开发,XHR 本身架构不清晰
- 针对MVC编程,不符合现在前端 MVVM 的浪潮
- 多个请求之间如果有先后关系的话,就会出现回调地狱
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好
-
axios
- 支持
PromiseAPI - 从浏览器中创建
XMLHttpRequest - 从
node.js创建http请求 - 支持请求拦截和响应拦截
- 自动转换 JSON 数据
- 客户端支持防止 CSRF/XSRF
- 支持
-
fetch
- 浏览器原生实现的请求方式,ajax 的替代品
- 基于标准
Promise实现,支持async/await - 只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
- 默认不会带
cookie,需要添加配置项 - 没有办法原生监测请求的进度,而 XHR 可以
什么是尾调用,使用尾调用有什么好处?
尾调用就是在函数的最后一步调用函数,在一个函数里调用另外一个函数会保留当前执行的上下文,如果在函数尾部调用,因为已经是函数最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。但是ES6的尾调用只能在严格模式下开启,正常模式是无效的。
用过哪些设计模式?
-
单例模式 保证类只有一个实例,并提供一个访问它的全局访问点。
-
工厂模式 用来创建对象,根据不同的参数返回不同的对象实例。
-
策略模式 定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
-
装饰器模式 在不改变对象原型的基础上,对其进行包装扩展。
-
观察者模式 定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它对对象都会得到通知。
-
发布订阅模式 基于一个主题/事件通道,希望接收通知的对象通过自定义事件订阅主题,被激活事件的对象(通过发布主题事件的方式被通知)。
实现寄生组合继承
利用Object.create()方法,将子类的原型指向父类,实现继承父类的方法属性,修改时也不影响父类。
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 执行父类构造函数
Parent.call(this, name);
this.age = age;
}
// 将子类的原型 指向父类
Child.prototype = Object.create(Parent.prototype);
// 此时的构造函数为父类的,需要指回自己
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
var child1 = new Child('Tom', 18);
child1.sayName(); // 'Tom'
child1.sayAge(); // 18
对闭包的理解以及使用场景
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
-
优点:
- 创建全局私有变量,避免变量全局污染
- 可以实现封装、缓存等
-
缺点:
- 创建的变量不能被回收,容易消耗内存,使用不当会导致内存溢出
- 解决:在不需要使用的时候把变量设为
null
-
使用场景:
- 用于创建全局私有变量
- 封装类和模块
- 隐藏数据,只提供api
- 实现函数柯里化,函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数
-
闭包一定会造成内存泄漏吗? 闭包并不一定会造成内存泄漏,如果在使用闭包后变量没有及时销毁,可能会造成内存泄漏的风险。只要合理的使用闭包,就不会造成内存泄漏。
防抖和节流用的是闭包吗?
防抖和节流可以使用闭包来实现。
- 防抖闭包实现原理:利用闭包可以让内部函数访问外部函数的变量这一特性。在防抖函数内部,返回一个新的函数。这个新函数会访问外部函数中的定时器变量。每次事件触发时,清除之前的定时器,然后重新设置一个新的定时器。这样就保证只有在最后一次触发后的延迟时间过后,才会执行真正的回调函数。
// timer变量被闭包捕获,return的函数可以访问和修改这个变量 function debounce(func, delay) { let timer; return function() { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { func.apply(this, arguments); }, delay); }; } - 节流闭包实现原理:同样是通过闭包来保存一个变量,这个变量用于记录上一次执行回调函数的时间。每次事件触发时,检查当前时间和上一次执行时间的间隔是否大于规定的单位时间,如果是,则执行回调函数,并更新上一次执行时间。
// lastTime变量被闭包捕获,用于控制回调函数的执行频率 function throttle(func, delay) { let lastTime = 0; return function() { let now = Date.now(); if (now - lastTime > delay) { func.apply(this, arguments); lastTime = now; } }; }
除了闭包,还有哪些实现防抖和节流的方式?
- 使用类(ES6 Classes)实现防抖:通过类的实例属性来保存定时器的状态,而不是依赖闭包
在这个示例中,Debounce 类的构造函数接受要执行的函数 func 和延迟时间 delay,同时初始化一个 timer 属性为 null。call 方法用于触发防抖操作,它和闭包实现防抖的逻辑类似,先清除之前的定时器,再重新设置一个新的定时器。class Debounce { constructor(func, delay) { this.func = func; this.delay = delay; this.timer = null; } call() { if (this.timer) { clearTimeout(this.timer); } this.timer = setTimeout(() => { this.func.apply(this, arguments); }, this.delay); } } // 使用示例 function handleClick() { console.log('按钮被点击'); } const debouncedClick = new Debounce(handleClick, 500); document.addEventListener('click', () => { debouncedClick.call(); }); - 使用类(ES6 Classes)实现节流:利用类的实例属性来记录上一次执行时间,以此来控制函数的执行频率
这里的 Throttle 类的构造函数接收要执行的函数 func 和间隔时间 interval,并初始化 lastTime 属性为 0。call 方法在每次被调用时,会检查当前时间和上一次执行时间的间隔是否大于等于设定的间隔时间,如果是,则执行函数并更新上一次执行时间。class Throttle { constructor(func, interval) { this.func = func; this.interval = interval; this.lastTime = 0; } call() { const now = Date.now(); if (now - this.lastTime >= this.interval) { this.func.apply(this, arguments); this.lastTime = now; } } } // 使用示例 function handleScroll() { console.log('页面正在滚动'); } const throttledScroll = new Throttle(handleScroll, 200); window.addEventListener('scroll', () => { throttledScroll.call(); });
对作用域、作用域链的理解
作用域:是一个变量或函数的可访问范围,作用域控制着变量或函数的可见性和生命周期。
-
全局作用域:可以全局访问
- 最外层函数和最外层定义的变量拥有全局作用域
window上的对象属性方法拥有全局作用域- 为定义直接复制的变量自动申明拥有全局作用域
- 过多的全局作用域变量会导致变量全局污染,命名冲突
-
函数作用域:只能在函数中访问使用
- 在函数中定义的变量,都只能在内部使用,外部无法访问
- 内层作用域可以访问外层,外层不能访问内存作用域
-
ES6中的块级作用域:只在代码块中访问使用
- 使用 ES6 中新增的
let、const声明的变量,具备块级作用域,块级作用域可以在函数中创建(由{}包裹的代码都是块级作用域) let、const声明的变量不会变量提升,const也不能重复申明- 块级作用域主要用来解决由变量提升导致的变量覆盖问题
- 使用 ES6 中新增的
作用域链:变量在指定的作用域中没有找到,会依次向一层作用域进行查找,直到全局作用域。这个查找的过程被称为作用域链。
call()、bind()和apply()的区别?
- 都可以用作改变
this指向 call和apply的区别在于传参,call和bind都是传入对象,apply传入一个数组call、apply改变this指向后会立即执行函数,bind在改变this后返回一个函数,不会立即执行函数,需要手动调用
连续多个bind,最后this指向是什么?
在 JavaScript 中,连续多次调用bind方法,最终函数的this上下文是由第一次调用`bind方法的参数决定的。
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const obj3 = { name: 'obj3' };
function getName() {
console.log(this.name);
}
const fn1 = getName.bind(obj1).bind(obj2).bind(obj3);
fn1(); // 输出 "obj1"
浏览器的垃圾回收机制
垃圾回收:JavaScript 代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间。如果不及时清理,会造成系统卡顿、内存溢出,这就是垃圾回收。 在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象:
- Major GC(主垃圾回收器):主要负责老生代垃圾的回收,内存占用比较小
- Minor GC(副垃圾回收器):主要负责新生代垃圾的回收,对象的占用空间大,对象存活时间长
新生代(副垃圾回收器)
副垃圾回收器主要负责新⽣代的垃圾回收。大多数的对象最开始都会被分配在新生代,该存储空间相对较小,分为两个空间:From空间(对象区)和To空间(空闲区)。
- 新增变量会放到
To空间,当空间满后需要执行一次垃圾清理操作 - 对垃圾数据进行标记,标记完成后将存活的数据复制到
From空间中,有序排列 - 交换两个空间,原来的
To变成From,旧的From变成To
老生代(主垃圾回收器)
主垃圾回收器主要负责⽼⽣代中的垃圾回收。存储一些占用空间大、存活时间长的数据,采用标记清除算法进行垃圾回收。 主要分为标记、清除两个阶段。
- 标记:将所有的变量打上标记0,然后从根节点(
window对象、DOM树等)开始遍历,把存活的变量标记为1 - 清除:清除标记为0的对象,释放内存。清除后将1的变量改为0,方便下一轮回收。
对⼀块内存多次执⾏标记清除算法后,会产⽣⼤量不连续的内存碎⽚。⽽碎⽚过多会导致⼤对象⽆法分配到⾜够的连续内存,于是⼜引⼊了另外⼀种算法——标记整理。 标记整理的标记过程仍然与标记清除算法⾥的是⼀样的,先标记可回收对象,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉这⼀端之外的内存。
引用计数法
一个对象被引用一次,引用数就+1,反之就-1。当引用为0,就会出发垃圾回收。 这种方式会产生一个问题,在循环引用时,引用数永远不会为0,无法回收。
哪些情况会导致内存泄漏
- 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数:设置了
setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 - 脱离
DOM的引用:获取一个DOM元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。 - 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
数据类型如何判定?
typeof:其中数组、对象、null都会被判断为object,其他判断都正确。instanceof:instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。constructor:console.log((2).constructor === Number); // trueObject.prototype.toString.call()
为什么0.1 + 0.2 !== 0.3,如何让其相等?
因为浮点数运算的精度问题。在计算机运行过程中,需要将数据转化成二进制,然后再进行计算。 因为浮点数自身小数位数的限制而截断的二进制在转化为十进制,就变成0.30000000000000004,所以在计算时会产生误差。 解决方案:
- 将其先转换成整数,再相加之后转回小数。具体做法为先乘10相加后除以10
let x=(0.1*10+0.2*10)/10; console.log(x===0.3) - 使用number对象的toFixed方法,只保留一位小数点
(n1 + n2).toFixed(2)
Object.is()与比较操作符===、==的区别?
- 使用双等号
==进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。 - 使用三等号
===进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。 - 使用
Object.is()来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
什么是 JavaScript 中的包装类型?
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
javascript 如何解决数值的精度问题?
- 问题产生的原因:在 JavaScript 中,浮点数的精度问题主要是由于其采用二进制的 IEEE 754 标准来表示数字。例如,像 0.1 和 0.2 这样的十进制小数在转换为二进制时会产生无限循环的二进制小数,计算机只能存储一个近似值。当进行计算时,这些近似值的误差就会累积,导致结果不准确。例如,
0.1+0.2在 JavaScript 中并不等于 0.3,而是等于 0.30000000000000004。 - 使用toFixed方法(简单的格式化)
- 原理:toFixed 方法是数字对象的一个方法,它可以将数字转换为指定小数位数的字符串表示形式。它会根据指定的小数位数进行四舍五入。
- 局限性:toFixed 方法返回的是一个字符串。如果需要进行后续的数学运算,还需要将其转换回数字。而且它只是一种格式化方法,并没有真正解决精度问题,只是在显示结果时进行了舍入处理。
- 手动调整数值表示方式(整数化处理)
- 原理:将小数转换为整数来避免浮点数运算产生的精度问题。这是基于整数运算在 JavaScript 中不存在浮点数那样的精度丢失情况。例如,对于涉及货币的计算,把金额的单位从元转换为分,这样就可以将小数乘法转换为整数乘法。
- 局限性:这种方法在处理复杂的运算时可能会变得繁琐。例如,当涉及除法、多次乘法和除法混合运算,或者需要处理不同精度的数值(如既有元又有角分)时,需要小心地进行单位转换和运算顺序安排,否则容易出错。
- 自定义精度控制函数(舍入和截断方法)
- 舍入方法原理:根据特定的规则对数值进行舍入,以控制精度。常见的舍入规则有四舍五入、向上舍入和向下舍入。通过 JavaScript 的数学函数和条件判断可以实现这些舍入规则。
- 截断方法原理:截断是指直接去掉多余的小数位,不进行舍入操作。可以通过将数值乘以适当的倍数转换为整数,然后再除以倍数来实现截断。
- 局限性:自定义的精度控制函数在处理复杂的数学公式或者多次运算时,需要仔细考虑舍入和截断的顺序对最终结果的影响。如果顺序不当,可能会导致累积误差,使结果偏离预期。
- 使用第三方库
bignumber.js:这个库提供了一种高精度的数字表示和计算方法。它将数字以字符串的形式存储,然后在内部使用自己的算法来进行精确的计算,包括加、减、乘、除等运算。例如,它会根据数字的位数和精度要求来处理计算,避免了 JavaScript 原生浮点数计算的精度损失。decimal.js:类似于bignumber.js,它也是专门用于高精度数字计算的库。它提供了更丰富的数学函数和操作,并且可以设置数字的精度、舍入模式等参数,以满足不同的计算需求。- 局限性:使用第三方库需要额外引入文件,增加了项目的复杂性和文件大小。并且,在与原生 JavaScript 数字进行交互时,需要进行类型转换等操作,可能会增加代码的复杂性。不过,对于对精度要求极高的金融等领域的计算,这些库是非常有效的解决方案。
js 函数中的 arguments 是什么?
arguments 是一个类数组的对象(伪数组),见的操作有三种:
- 获取参数的长度
- 根据索引值获取某一个参数
- callee 获取当前 arguments 所在的函数
forEach 和 map 的区别,可以改变原数组吗?
- forEach() 方法没有返回值,而 map() 方法有返回值。
- forEach遍历通常都是直接引入当前遍历数组的内存地址,会改变原数组,生成的数组的值发生变化,当前遍历的数组对应的值也会发生变化。类似于浅拷贝。
- map遍历的后的数组通常都是生成一个新的数组,新的数组的值发生变化,当前遍历的数组值不会变化。地址和值都改变,类似于深拷贝。
- 总的来说 map 的速度大于 forEach,性能上来说 for > forEach > map。for > forEach 因为forEach每次都要创建一个函数来调用,而for不会创建函数,函数需要独立的作用域,会有额外开销.
什么时候用 href,什么时候用 src ?
- src指向的内容会嵌入到文档中当前标签所在的位置。常用的有:img、script、iframe。
- href是Hypertext Reference的缩写,表示超文本引用。用来建立当前元素和文档之间的链接。href 目的不是为了引用资源,而是为了建立联系,让当前标签能够链接到目标地址。常用的有:link、a。
- 总结: src用于替换当前元素(比如:引入一张图片);href用于在当前文档和引用资源之间建立联系。
扩展运算符的使用场景?
- 复制数组
const arr2 = [...arr1],但不适用于多级数组或带有日期或函数的数组。 - 合并数组
const arr3 = [...arr1, ...arr2] - 向数组中添加元素
arr1 = [...arr1, 'array'] - 向对象添加属性
const output = {...user, age: 31} - 解构对象
const {firstName, ...rest} = userObj - 向函数传递无限参数
const myFunc = (...args) => { console.log(args); };
Event Loop 事件循环
事件循环指的是 js 代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。
微任务&宏任务:
- js 代码主要分为两大类:同步代码 与 异步代码
- 异步代码又分为:微任务 与 宏任务
- 宏任务:事件、网络请求、
setTimeout()定时器、fs.readFile()读取文件 - 微任务:
Promise.then()、async/await
事件循环 Event Loop 执行机制:
- 进入到script标签,就进入到了第一次事件循环.
- 遇到同步代码,立即执行
- 遇到宏任务,放入到宏任务队列里.
- 遇到微任务,放入到微任务队列里.
- 执行完所有同步代码
- 执行微任务代码
- 微任务代码执行完毕,本次队列清空
- 寻找下一个宏任务,重复步骤1
以此反复直到清空所有宏任务,这种不断重复的执行机制,就叫做事件循环。
为什么需要事件循环这个机制?
- 单线程特性:JavaScript 是单线程语言,这意味着在同一时间只能执行一段代码。如果没有事件循环,当遇到耗时的操作(如网络请求、文件读取等)时,整个程序就会被阻塞,无法响应其他操作,导致用户体验极差。事件循环可以在这些耗时操作执行的同时,继续处理其他任务,保证程序的响应性。
- 异步编程:现代 Web 应用中,异步操作非常常见。比如,当用户点击一个按钮发起一个网络请求时,程序不需要等待请求返回就可以继续执行其他任务。事件循环使得异步操作成为可能,它可以在异步任务完成后,将相应的回调函数加入任务队列,等待主线程空闲时执行。
- 提高效率:通过事件循环,JavaScript 引擎可以更加高效地利用 CPU 资源。在等待异步任务完成的过程中,引擎可以执行其他任务,而不是浪费时间等待。这样可以提高程序的整体性能和响应速度。
setInterval 和 setTimeout 的区别
- 触发方式
- setTimeout:在指定的延迟时间后执行一次指定的函数。只执行一次。
- setInterval:按照指定的时间间隔重复执行指定的函数。持续执行,除非被清除。
- 清除方式
- 对于 setTimeout,一般不需要专门清除,因为它只执行一次。但如果在某些情况下需要取消尚未执行的 setTimeout,可以使用 clearTimeout。
- 对于 setInterval,可以使用 clearInterval 来停止正在重复执行的定时器。
- 应用场景
- setTimeout:适用于延迟执行某个一次性的操作,比如在用户点击按钮后一段时间后显示提示信息。
- setInterval:适用于需要定期执行的任务,比如轮询服务器获取数据更新。
setInterval 和递归 setTimeout 有什么区别?
- 执行方式
- setInterval:在指定的时间间隔重复执行回调函数,是一种自动重复的机制。一旦设置,它会按照固定的时间间隔不断触发回调,除非被手动清除。
- 递归 setTimeout:通过在回调函数内部再次调用 setTimeout 来实现重复执行。每次执行完当前任务后,等待指定时间再触发下一次执行。
- 准确性
- setInterval:在某些情况下可能会出现执行时间累积的问题,导致实际执行间隔小于预期。例如,如果回调函数执行时间较长,下一次执行可能会在上一次执行还未完成时就开始,从而导致执行频率加快。
- 递归 setTimeout:由于每次都是在上一次执行完成后再设置下一次的延迟,所以可以更准确地控制执行间隔,避免时间累积的问题。
- 停止方式
- setInterval:可以使用clearInterval来停止重复执行。
- 递归 setTimeout:通常可以在回调函数内部设置一个条件来停止递归调用,从而停止重复执行。
Preload 和 Prefetch 是什么?有什么区别?
- preload 是一个 HTML 关键字,用于告诉浏览器某个资源是当前页面后续渲染所必需的,因此应该尽早加载。这通常用于加载那些对首屏渲染至关重要的资源,比如 CSS、JavaScript 文件或字体。使用 preload 的典型例子包括:
- 预先加载页面的 CSS 样式表,以确保样式能够及时应用。
- 预先加载关键的 JavaScript 文件,这些文件可能包含对页面交互性至关重要的代码。
- 预先加载字体文件,以防止文本在页面加载过程中出现“闪烁”。
- prefetch 则用于告诉浏览器某个资源在当前页面可能不会用到,但在未来某个时刻,用户可能会访问到的页面中会用到。这通常用于加载那些不是当前页面必需,但对后续页面导航可能有用的资源。prefetch 的典型用例包括:
- 预先加载下一个页面或路由所需的资源,比如在用户可能即将导航到的新页面的 CSS 或 JavaScript。
- 预先加载可能需要的资源,比如用户在当前页面上执行某个操作后可能会用到的资源。
区别和使用建议:
- preload 用于当前页面肯定会用到的资源,而 prefetch 用于可能在后续页面中用到的资源。
- preload 通常用于首屏渲染的关键资源,而 prefetch 用于优化页面跳转的性能。
- 应该谨慎使用 prefetch,因为它可能会加载用户实际上并不需要的资源,从而浪费带宽。
- preload 资源通常会被赋予较高的加载优先级,而 prefetch 资源的加载优先级较低,通常会在页面的其他资源加载完毕后才会被加载。
如何判断一个资源是否应该使用 preload 或 prefetch?
- 使用 preload 的情况
- 关键资源:如果某个资源对于当前页面的首屏渲染至关重要,比如主要的 CSS 样式表或 JavaScript 文件,应该使用 preload。
- 立即可用性:对于那些需要立即可用以避免渲染阻塞的资源,如字体文件,使用 preload 可以确保它们在渲染文本前被加载。
- 确定性:如果你可以确定某个资源将在页面加载过程中被使用,那么应该使用 preload 来提升其加载优先级。
- 性能瓶颈:对于可能会成为性能瓶颈的资源,如大型 JavaScript 框架或库,使用 preload 可以减少页面交互前的加载时间。
- 使用 prefetch 的情况
- 导航预测:如果你能预测用户可能导航到的下一个页面,并且可以提前加载该页面的资源,比如下一页的 CSS 或 JavaScript,那么应该使用 prefetch。
- 低优先级资源:对于那些不直接影响当前页面性能,但对未来页面性能有影响的资源,使用 prefetch 可以在浏览器空闲时加载。
- 用户行为预测:基于用户行为模式,如果你能预测用户可能会执行某些操作,从而需要额外的资源,比如点击某个按钮后需要加载的模块,可以使用 prefetch。
- 缓存利用:对于那些可能会被缓存并且在用户后续导航中使用的资源,使用 prefetch 可以提前将它们加载到缓存中。
- 判断流程
- 分析页面依赖:审查页面的 HTML、CSS 和 JavaScript,确定哪些资源是渲染和功能所必需的。
- 确定资源用途:明确资源是在当前页面使用还是可能在后续页面使用。
- 评估加载时机:考虑资源加载的时机是否会影响用户体验,比如是否会造成首屏渲染延迟。
- 考虑性能影响:评估过早加载资源是否会导致不必要的带宽消耗或延迟其他重要资源的加载。
- 测试和验证:使用浏览器的开发者工具模拟不同的网络条件,观察资源加载对性能的实际影响。
- 注意事项
- 不要过度使用:过度使用 preload 和 prefetch 可能会导致不必要的资源加载,增加服务器负载和用户的数据使用量。
- 监控和优化:持续监控页面性能,根据实际数据调整资源的预加载策略。
- 移动设备考虑:在移动设备上,由于带宽和数据使用量的限制,更应谨慎使用预加载技术。
Number(null)和Number(undefined)的结果是什么?
Number(null)返回0- 从概念上来说,null 代表的是一个空值,没有具体的数值含义。在进行类型转换时,JavaScript 的设计者认为将 null 转换为数字 0 是一种较为合理的默认行为。
- 从实际转换过程来看,当 Number 方法处理 null 值时,它会遵循特定的转换逻辑。首先尝试调用 null 值的 valueOf 方法,由于 null 没有这个方法,于是接着尝试调用 toString 方法,null 的 toString 方法返回字符串 null。但是这个字符串无法被解析为数字,所以按照规则,最终返回 0。
- 这样的设计可以在一些情况下简化代码的编写,避免在处理可能为 null 的值时出现意外的结果。例如,在进行一些数学运算时,如果某个变量可能为 null,而我们期望在这种情况下得到一个默认的数字值,就可以利用这个特性。
Number(undefined)返回NaN- undefined 表示一个未初始化的变量或者不存在的属性。它没有一个明确的数值含义,不像 null 被设计为在某些情况下可以转换为 0。
- 当 Number 方法处理 undefined 值时,首先尝试调用 undefined 的 valueOf 方法,由于 undefined 没有这个方法,返回 undefined;接着尝试调用 toString 方法,同样,undefined 的 toString 方法也返回 undefined;由于无法从 undefined 得到一个可以解析为数字的字符串或者原始值,按照规则,最终返回NaN。
- 这种设计是为了明确表示对 undefined 进行数字转换是一个无效的操作。NaN 的含义是
Not a Number,即不是一个数字。它可以在后续的代码中被用来判断某个值是否是通过无效的转换得到的,以便开发者进行适当的错误处理。例如,在进行一些复杂的计算时,如果某个变量可能为undefined,而我们没有对其进行适当的检查,直接进行数字运算,得到NaN的结果可以提醒我们可能存在错误的输入。
如何判断一个元素在不在可视区内?
- 使用
getBoundingClientRect方法- getBoundingClientRect 方法返回一个 DOMRect 对象,该对象提供了元素的大小及其相对于视口的位置信息。
- 通过检查元素的顶部、底部、左侧和右侧边缘与视口的关系,可以确定元素是否在可视区内。
function isElementInViewport(element) { const rect = element.getBoundingClientRect() return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ) } - 计算元素位置和视口大小比较
- 通过获取元素的位置和大小,以及视口的大小,进行比较来判断元素是否在可视区内。
- 可以使用 offsetTop、offsetLeft、offsetHeight、offsetWidth 等属性来获取元素的位置和大小信息。
function isElementInViewportByCalculation(element) { const viewportHeight = window.innerHeight || document.documentElement.clientHeight const viewportWidth = window.innerWidth || document.documentElement.clientWidth const elementTop = element.offsetTop const elementLeft = element.offsetLeft const elementBottom = elementTop + element.offsetHeight const elementRight = elementLeft + element.offsetWidth return ( elementTop >= 0 && elementLeft >= 0 && elementBottom <= viewportHeight && elementRight <= viewportWidth ) } - Intersection Observer API
- Intersection Observer API 提供了一种异步观察目标元素与祖先元素或顶级文档视窗交叉状态的方法。
- 可以使用它来判断目标元素是否进入或离开可视区,而无需在每一帧都进行位置检查,从而提高性能。
function isElementInViewportWithObserver(element) { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { console.log('元素在可视区内') } else { console.log('元素不在可视区内') } }) }) observer.observe(element) }
JS严格模式有什么特点?
- 全局变量必须声明
- 禁止this指向windows
- 函数参数名称不能重复
- 禁止使用with
- 创建eval作用域(单独作用域)
ES5 和 ES6 如何实现继承?
ES5:
- 原型链继承
- 构造函数继承
- 组合继承
- 寄生组合继承
ES6:ES6中引入了class关键字,class可以通过extends关键字实现继承。ES6 的继承中super是用来调用父类函数并指向父类的原型
有哪些继承的方式?
- 原型链继承
- 原理:利用原型链,让一个构造函数的原型对象等于另一个类型的实例。这样,子类型的实例就能够访问父类型原型上的属性和方法。
- 示例代码:
function Parent() { this.name = 'parent'; } Parent.prototype.sayName = function () { console.log(this.name); }; function Child() {} Child.prototype = new Parent(); var child = new Child(); child.sayName(); // 输出 'parent' - 缺点:
- 所有子类型的实例共享父类型实例的属性。如果一个子类型的实例修改了从父类型继承来的引用类型的属性,会影响到其他子类型的实例。
- 在创建子类型的实例时,无法向父类型的构造函数传递参数。
- 构造函数继承(借助 call 或 apply)
- 原理:在子类型的构造函数中通过
call()或apply()方法调用父类型的构造函数,将父类型的构造函数中的this绑定到子类型的实例上,这样就可以在子类型的实例上复制父类型的属性。 - 示例代码:
function Parent(name) { this.name = name; } function Child(name) { Parent.call(this, name); } var child = new Child('child'); console.log(child.name); // 输出 'child' - 缺点:
- 这种方式只能继承父类型构造函数中的属性,不能继承父类型原型上的方法。每个子类型的实例都会复制一份父类型的属性,无法实现函数复用,造成内存浪费。
- 原理:在子类型的构造函数中通过
- 组合继承(原型链继承 + 构造函数继承)
- 原理:结合了原型链继承和构造函数继承的优点。通过构造函数继承来继承父类型的属性,再通过原型链继承来继承父类型原型上的方法。
- 示例代码:
function Parent(name) { this.name = name; } Parent.prototype.sayName = function () { console.log(this.name); }; function Child(name) { Parent.call(this, name); } Child.prototype = new Parent(); Child.prototype.constructor = Child; var child = new Child('child'); child.sayName(); // 输出 'child' - 缺点:
- 调用了两次父类型的构造函数(一次是在
Child.prototype = new Parent(),另一次是在 Child 构造函数内部通过 call 调用),造成了一定的性能开销。
- 调用了两次父类型的构造函数(一次是在
- 原型式继承
- 原理:基于已有的对象创建一个新对象,同时让新对象的原型指向这个已有的对象。可以使用
Object.create()方法来实现。 - 示例代码:
var parent = { name: 'parent', sayName: function () { console.log(this.name); } }; var child = Object.create(parent); child.name = 'child'; child.sayName(); // 输出 'child' - 缺点:
- 与原型链继承类似,所有通过这种方式创建的对象共享原型对象上的属性。如果原型对象上的属性是引用类型,一个对象对其修改会影响到其他对象。
- 原理:基于已有的对象创建一个新对象,同时让新对象的原型指向这个已有的对象。可以使用
- 寄生式继承
- 原理:在原型式继承的基础上,增强对象的功能。创建一个函数,在函数内部以某种方式来增强对象,最后返回这个对象。
- 示例代码:
var parent = { name: 'parent' }; function createChild(obj) { var clone = Object.create(obj); clone.sayName = function () { console.log(this.name); }; return clone; } var child = createChild(parent); child.sayName(); // 输出 'parent' - 缺点:
- 跟原型式继承一样,多个实例之间共享原型对象的属性,而且这种方式主要是为了特定的对象创建包装函数,没有从根本上解决继承的问题,代码复用性不高。
- 寄生组合继承
- 原理:这是最理想的继承方式。它通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,避免了组合继承中两次调用父类型构造函数的问题。
- 示例代码:
function Parent(name) { this.name = name; } Parent.prototype.sayName = function () { console.log(this.name); }; function Child(name) { Parent.call(this, name); } function inheritPrototype(child, parent) { var prototype = Object.create(parent.prototype); prototype.constructor = child; child.prototype = prototype; } inheritPrototype(Child, Parent); var child = new Child('child'); child.sayName(); // 输出 'child' - 优点:
- 这种方式解决了组合继承的缺点,只调用一次父类型的构造函数,并且能够正确地继承父类型原型上的方法,是比较推荐的一种继承方式。
DOM
children方法和childNodes方法有什么区别?
children 方法和 childNodes 方法都是用于获取 DOM 节点的子节点集合,但它们之间存在一些区别:
- childNodes:返回一个 NodeList 对象,它包含指定节点的所有子节点,包括元素节点、文本节点、注释节点等。
- children:也返回一个 HTMLCollection 对象,但它只包含指定节点的元素子节点。
<div id="parent">
<p>Paragraph 1</p>
Text node
<p>Paragraph 2</p>
</div>
- 使用
parentNode.childNodes获取 id 为 parent 的元素的子节点,会得到一个包含三个子节点的 NodeList,分别是两个<p>元素节点和一个文本节点(Text node)。 - 使用
parentNode.children获取子节点时,只会得到一个包含两个<p>元素节点的 HTMLCollection。
事件委托
事件委托是指将事件绑定目标元素的到父元素上,利用冒泡机制触发该事件。 优点:可以减少事件注册,节省大量内存占用;可以将事件应用于动态添加的子元素上。
- 错误版(但是可能能过)
bug在于,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对。ul.addEventListener('click', function(e){ if(e.target.tagName.toLowerCase() === 'li'){ fn() // 执行某个函数 } }) - 高级版
思路是点击 span 后,递归遍历 span 的祖先元素看其中有没有 ul 里面的 li。function delegate(element, eventType, selector, fn) { element.addEventListener(eventType, e => { let el = e.target while (!el.matches(selector)) { if (element === el) { el = null break } el = el.parentNode } el && fn.call(el, e, el) }) return element }
用 mouse 事件写一个可拖曳的 div
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>可拖曳的 div</title>
</head>
<body>
<div id="xxx"></div>
<script>
var dragging = false
var position = null
xxx.addEventListener('mousedown',function(e){
dragging = true
position = [e.clientX, e.clientY]
})
document.addEventListener('mousemove', function(e){
if(dragging === false){return}
console.log('hi')
const x = e.clientX
const y = e.clientY
const deltaX = x - position[0]
const deltaY = y - position[1]
const left = parseInt(xxx.style.left || 0)
const top = parseInt(xxx.style.top || 0)
xxx.style.left = left + deltaX + 'px'
xxx.style.top = top + deltaY + 'px'
position = [x, y]
})
document.addEventListener('mouseup', function(e){
dragging = false
})
</script>
</body>
</html>
DOM 事件级别
- DOM0
onXXX 类型的定义事件
element.onclick = function(e) { ... } - DOM2
addEventListener 方式
element.addEventListener('click', function (e) { ... }) - DOM3
增加了很多事件类型
element.addEventListener('keyup', function (e) { ... })
DOM 事件模型
捕获从上到下, 冒泡从下到上。 先捕获,再到目标,再冒泡。
DOM 事件流
DOM 标准采用捕获+冒泡。两种事件流都会触发DOM的所有对象,从 window 对象开始,也在 window 对象结束。 事件流包括三个阶段:
- 捕获阶段(Capturing phase):事件从最外层的祖先元素开始向目标元素传播。这个阶段的目的是在事件到达目标元素之前,给父元素机会优先处理事件。例如,当你点击一个按钮时,事件首先从文档的根元素开始向下传播,依次经过每个父元素,直到到达目标按钮。
- 目标阶段(Target phase):事件到达触发它的目标元素。在这个阶段,特定于目标元素的事件处理程序被执行。
- 冒泡阶段(Bubbling phase):如果事件没有在目标阶段被完全处理,事件会从目标元素开始向上冒泡回祖先元素。这个过程与捕获阶段相反,从目标元素开始,依次向上触发每个父元素的事件处理程序,直到到达文档的根元素。
如何阻止事件冒泡?
- 使用
event.stopPropagation()方法 - 在一些情况下,特别是对于使用 jQuery 等库时,在事件处理函数中返回 false 可以阻止事件冒泡,还可以阻止默认行为
event 对象常见应用
event.target触发事件的元素。event.currentTarget绑定事件的元素。event.preventDefault()阻止默认行为。event.stopPropagation()阻止在捕获阶段或冒泡阶段继续传播,而不是阻止冒泡。event.stopImmediatePropagation()阻止事件冒泡并且阻止相同事件的其他侦听器被调用。
DOM 操作
- 创建新节点
createDocumentFragment()创建一个 DOM 片段createElement()创建一个具体的元素createTextNode()创建一个文本节点
- 添加、移除、替换、插入
appendChild()removeChild()replaceChild()insertBefore()
- 查找
getElementsByTagName()getElementsByName()getElementById()
document.write 和 innerHTML 的区别
- document.write 只能重绘整个页面
- innerHTML 可以重绘页面的一部分
window 对象和 document 对象
- window 对象
- 表示当前浏览器的窗口,是 JavaScript 的顶级对象。
- 我们创建的所有对象、函数、变量都是 window 对象的成员。
- window 对象的方法和属性是在全局范围内有效的。
- document 对象
- 是 HTML 文档的根节点。
- 使我们可以通过脚本对 HTML 页面中的所有元素进行访问。
- document 对象是 window 对象的一部分(window.document)。
区分什么是 客户区坐标、页面坐标、屏幕坐标
- 客户区坐标 鼠标指针在可视区中的水平坐标(clientX)和垂直坐标(clientY)。
- 页面坐标 鼠标指针在页面布局中的水平坐标(pageX)和垂直坐标(pageY)。
- 屏幕坐标 设备物理屏幕的水平坐标(screenX)和垂直坐标(screenY)。
事件触发三阶段
- window 往事件触发处传播,遇到注册的捕获事件会触发
- 传播到事件触发处时触发注册的事件
- 从事件触发处往 window 传播,遇到注册的冒泡事件会触发
事件对象中 target 和 currentTarget 的区别
- target:是触发事件的某个具体的对象。
- currentTarget:是绑定事件的对象。
BOM
BOM 与 DOM
- BOM 浏览器对象模型。主要处理浏览器窗口和框架,描述了与浏览器进行交互的方法和接口,可以对浏览器窗口进行访问和操作。
- BOM 与 DOM 的关系
- JavaScript 是通过访问 BOM 对象来访问、控制、修改浏览器
- BOM 的 window 包含了 document,因此通过 window 对象的 document 属性就可以访问、检索、修改文档内容与结构 所以,BOM 包含了 DOM,浏览器提供出来给予访问的是 BOM 对象,从 BOM 对象再访问到 DOM 对象,从而 JS 可以操作浏览器以及浏览器读取到的文档。
BOM 对象包含哪些内容?
- window 对象:JavaScript 层级中的顶层对象,表示浏览器窗口。
- navigator 对象:包含客户端浏览器的信息。
- history 对象:包含了浏览器窗口访问过的 URL。
- location 对象:包含了当前 URL 的信息。
- screen 对象:包含客户端显示屏的信息。
offsetWidth/offsetHeight、clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别
- offsetWidth/offsetHeight 返回值包含 content + padding + border,效果与 e.getBoundingClientRect() 相同。
- clientWidth/clientHeight 返回值只包含 content + padding,如果有滚动条,也不包含滚动条。
- scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸