面试题
基础概念
Promise 原理及用法
async/await 原理
es6 Generator 原理
Map 数据结构
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。
Set 数据结构
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
Map 和 Object 的区别
Map 默认不包含任何键,只包含显式插入的键。Object 上会有原型上的键。
Map 的键是有序的,而 Object 的键是无序的。
Map 的键可以是任意值,包括函数、对象或任何基本类型。Object 的键必须是字符串或符号。
Map 的键是唯一的,不会重复。Object 的键是唯一的,如果重复,后面的会覆盖前面的。
Map 的键可以进行迭代,而 Object 的键需要通过 Object.keys() 或 for...of 循环来迭代。
Map 的键值对可以通过 size 属性来获取,而 Object 的键值对需要通过 Object.keys() 或 for...of 循环来获取。
Map 的键值对可以通过 set() 方法来设置,而 Object 的键值对需要通过赋值操作来设置。
Map 的键值对可以通过 get() 方法来获取,而 Object 的键值对需要通过属性访问来获取。
Map 的键值对可以通过 delete() 方法来删除,而 Object 的键值对需要通过 delete
Map 在频繁增删键值对时性能更好,而 Object 在查找键值对时性能更好。
Proxy 和 Reflect
Proxy(代理) 是 ES6 中新增的一个特性。Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。 Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
symbol 数据类型
Symbol 是 ECMAScript 6 引入的一种新的原始数据类型,用来表示独一无二的值。每个 Symbol 值都是唯一的,因此可以用来创建一些独特的标识符。
Symbol 值通过 Symbol 函数生成。
let s = Symbol();
Symbol 函数前不能使用 new 命令,否则会报错。这是因为 Symbol 是原始数据类型,不是对象。
Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames() 返回。
使用场景:
- 常量的定义
const MY_CONST = Symbol('my_const')
- 对象的属性名
const obj = {};
const mySymbol = Symbol("my symbol");
obj[mySymbol] = "hello";
console.log(obj[mySymbol]); // 输出:'hello'
- 定义只读属性
const obj = {};
const mySymbol = Symbol("my symbol");
Object.defineProperty(obj, mySymbol, {
value: "hello",
writable: false,
});
console.log(obj[mySymbol]); // 输出:'hello'
obj[mySymbol] = "world";
console.log(obj[mySymbol]); // 输出:'hello'
- 私有属性
const _myPrivateProp = Symbol("my_private_prop");
class MyClass {
constructor() {
this[_myPrivateProp] = "private value";
}
getPrivateValue() {
return this[_myPrivateProp];
}
}
call、apply、bind 的区别和用法
call 方法可以改变函数的 this 指向,同时还能传递多个参数。
apply 方法和 call 方法类似,它也可以改变函数的 this 指向,但是它需要传递一个数组作为参数列表。
bind 方法和 call、apply 方法不同,它并不会立即调用函数,而是返回一个新的函数,这个新函数的 this 指向被绑定的对象。
let 和 const 与 var 的区别
1、不存在变量提升 必须先定义后使用,否则报错
2、暂时性死区 在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
3、不允许重复申明/不允许在函数内部重新申明参数(也算重复申明)
4.1 SE5 的作用域 1)、内层变量覆盖外层的变量 2)、用来计数的循环变量会泄露为全局变量
5、const 是一个常量,一旦声明,就不能改变。而且在申明的时候必须初始化,不能留到后面赋值。
6、在 ES5 里面,var 在全局作用域下申明的变量,会自动生为 window 的属性: 没法在编译过程爆出变量为申明的错误,语法上顶层对象有一个实体含义的对象这样肯定不合适。 用 var 定义的依然会升级为顶层对象(全局对象)window 的属性;但是 let,const 申明则不会。
js 数据类型
在 JavaScript
中,数据类型可以分为两类:原始类型和对象类型。原始类型包括:数字(number
)、字符串(string
)、布尔值(boolean
)、null
、undefined
、 Symbol
(ES6 新增)和 BigInt
,对象类型包括:对象(object
)、数组(array
)、函数(function)等。
基础类型:数字、字符串、布尔值、null 和 undefined 是 JavaScript 中的五种原始类型,它们都是不可变的。每次对原始类型进行操作时,都会创建一个新的原始类型的值
对象类型: 对象类型则是可变的,因为对象、数组、函数等值是通过引用来访问的
常见数组排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 归并排序
- 堆排序
解释一下 原型、构造函、实例、原型链 之间的关系
构造函数可以通过 new 来生成一个实例、构造函数也是函数; 函数都有一个 prototype 属性,这个就是原型对象; 原型对象可以通过构造器 constructor 来指向它的构造函数; 实例的proto属性,指向的是其构造函数的原型对象;
原型链:从一个实例对象,向上找构造这个实例相关联的对象,相关联的对象又向上找,找到创造它的一个实例对象, 一直到 Object.prototype 截止。原型链是通过 prototype 和proto向上找的。构造函数通过 prototype 创建了很多方法, 被其所有实例所公用,存放在原型对象上;
instanceof 原理
实例对象上面有一个proto ,这个是引用的它构造函数的原型对象;
instanceof 是用来判断实例是不是由某个构造函数实例化出来的对象,其原理是判断实例对象是否指向构造函数的原型; 只要是在原型链上的函数,都会被 instanceof 看做是实例对象的一个构造函数,所以都会返回 true;
new 运算符 后面跟着的是一个构造函数,使一个新对象被创建。
Object.create()
创建的实例对象是指向的对象原型,实例对象本身是不具备创建对象的属性和方法的,是通过原型链来链接的。
DOM 事件
DOM 事件模型、DOM 事件流、DOM 事件捕获的具体流程、Event 对象的常见应用、自动触发事件
常见 DOM 事件
- UI 事件,当用户与页面上的元素交互时触发,如:load、scroll
- 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
- 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
- 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
- 文本事件,当在文档中输入文本时触发,如:textInput
- 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
DOM 事件流
DOM 事件流指的是事件从文档的顶层流动到底层,然后返回的过程。事件流分为两个阶段:事件捕获和事件冒泡。
捕获(从上到下)、冒泡(从下到上);
捕获->目标阶段->冒泡
js 闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。
示例
function makeAdder(x) {
return function (y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
js 闭包的用途
- 保持变量私有
function createCounter() {
let count = 0; // 私有变量
function increment() {
count++;
console.log(count);
}
return increment;
}
const counter = createCounter();
counter(); // 输出: 1
counter(); // 输出: 2
- 模拟模块化
防止污染全局变量
const moduleA = (function () {
const privateData = "private";
function privateMethod() {
console.log(privateData);
}
return {
publicMethod: function () {
privateMethod();
},
};
})();
moduleA.publicMethod(); // 输出: 'private'
- 保存状态
闭包可以用来保存函数之间共享的状态,这种状态可以在多次调用之间持久化。
function createAdder(x) {
return function (y) {
return x + y;
};
}
const add5 = createAdder(5);
console.log(add5(3)); // 输出: 8
js 中的 this 指向
箭头函数的 this 指向声明时所在作用域下的 this 值。
- 所有的 this 关键字,在函数运行时,才能确定它的指向
- this 所在的函数由哪个对象调用,this 就会指向谁
- 在函数调用中 this 指向 window 对象
- 在对象中调用函数 this 指向调用这个函数的对象
- 在构造函数中 this 指向实例出来的对象
- 在 call apply bind 中 this 指向第一个参数,bind 特殊,需要手动执行这个函数。
- 箭头函数中的 this 指向调用这个函数的外层对象。有时是 window. (箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。
宏任务和微任务
在 JavaScript 中,宏任务(macro-task)和微任务(micro-task)是指异步操作的两种类型。
宏任务通常包括以下操作:
setTimeout 和 setInterval 定时器回调函数
事件回调函数(例如,鼠标点击、键盘输入等)
AJAX 请求的回调函数
请求动画帧(requestAnimationFrame)回调函数
script 标签的 onload 和 onerror 事件
当一个宏任务开始执行时,JavaScript 引擎会将其放入调用堆栈的底部,然后继续执行其他代码。当调用堆栈为空时,JavaScript 引擎会取出下一个宏任务并执行。
微任务通常包括以下操作:
Promise 的回调函数
Generator 函数
MutationObserver 的回调函数
process.nextTick(Node.js 环境下)
通常情况下,微任务的优先级高于宏任务。
当一个宏任务开始执行时,如果在它的执行过程中产生了微任务,那么这些微任务会被添加到微任务队列中,等待当前宏任务执行完成后立即执行。如果在这个过程中产生了新的微任务,则会一直执行微任务,直到微任务队列为空,然后 JavaScript 引擎才会继续执行下一个宏任务。
ES6 模块和 commonjs 模块的差异
差异:
- CommonJS 输出是值的拷贝,即原来模块中的值改变不会影响已经加载的该值,ES6 静态分析,动态引用,输出的是值的引用,值改变,引用也改变,即原来模块中的值改变则该加载的值也改变
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 加载的是整个模块,即将所有的接口全部加载进来,ES6 可以单独加载其中的某个接口(方法)
- CommonJS this 指向当前模块,ES6 this 指向 undefined
特点 | ES6 模块 | CommonJS 模块 |
---|---|---|
语法 | 使用import 和export 语法 | 使用require 和module.exports 语法 |
动态导入 | 支持动态导入,可以根据条件导入不同的模块 | 不支持动态导入,导入的模块在脚本加载时确定 |
导入和导出的类型 | 可以导入和导出变量、函数、类、默认导出等多种类型 | 只能导入和导出整个模块对象 |
导入方式 | 可以使用命名导入和默认导入方式 | 只支持命名导入方式 |
导出方式 | 可以使用命名导出和默认导出方式 | 只支持命名导出方式 |
模块加载时机 | 在编译时就会生成所有模块的依赖关系,可以进行静态分析 | 在运行时加载模块,无法进行静态分析 |
模块间的关系 | 每个 ES6 模块都有自己的作用域,相互之间没有依赖关系 | 模块之间共享相同的作用域,可以直接访问和修改导出的变量和函数 |
浏览器支持 | 部分浏览器原生支持,可以使用 Babel 转译实现兼容性 | 不支持,需要使用工具如 Browserify、Webpack 进行转译和打包 |
Node.js 使用 | 需要使用--experimental-modules 标志启用 ES 模块支持 | 原生支持 CommonJS 模块 |
如何避免重排重绘
任何改变用来构建渲染树的信息都会导致一次重排或重绘:
- 添加、删除、更新 DOM 节点
- 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
- 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
- 移动或者给页面中的 DOM 节点添加动画
- 添加一个样式表,调整样式属性
- 用户行为,例如调整窗口大小,改变字号,或者滚动
如何避免:
- 集中样式改变合并多次操作
- 离屏幕计算
- 减少通配符的使用
前端错误监控
错误分类:运行错误(代码错误)、资源加载错误
try...catch
window.onerror
资源加载错误:
- object.onerror()
js 内存泄漏如何排查
https://github.com/zhansingsong/js-leakage-patterns
被console
使用的对象是不能被垃圾回收的,这就可能会导致内存泄漏。
被全局变量、全局函数引用的对象,在Vue组件销毁时未清除,可能会导致内存泄漏
定时器未及时在Vue组件销毁时清除,可能会导致内存泄漏
绑定的事件未及时在Vue组件销毁时清除,可能会导致内存泄漏
被自定义事件引用,在Vue组件销毁时未清除,可能会导致内存泄漏
虚拟 DOM
浏览器操作 dom 是花销非常大的。执行 JS 花销要小非常多,所以在复杂场景下使用虚拟 DOM+diff 算法能更高效的执行。
Iterator 和 for...of
TS项目使用 alias path
https://www.miganzi.com/tech/typescript-s-paths-config/
函数尾调用
在函数的执行过程中,如果最后一个动作是一个函数的调用, 即这个调用的返回值被当前函数直接返回,则称为尾调用。
function f(x){
return g(x);
}
js this 的使用场景
this 是执行上下文中的一个属性,它指向最后一次调用这个对象的方法。
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
第二种方法调用模式,如果函数作为一个对象的方法来调用时,this 指向这个对象。
第三种构造器调用模式,如果一个函数用new 调用时,函数执行会新创建一个对象, this 指向这个新创建的对象。
第四种是 apply、call 、 bind 调用模式。
Map 和 weakMap 的区别
Map 和 WeakMap 都是 JavaScript 中的键值对数据结构,它们的主要区别在于其键的存储方式和内存管理。
箭头函数和普通函数的区别
语法: 箭头函数使用 ()=> ,普通函数使用 function 关键字来定义函数。
箭头函数没有自己的this,它会继承其所在作用域的this 值。而普通函数的 this 则由函数调用时的上下文决定,可以通过 call、apply、bind 方法来改变。
箭头函数没有自己的 arguments 对象,它可以通过 rest 参数语法来接收不定数量的参数。而普通函数则有自己的 arguments 对象,它可以接收任意数量的参数。
箭头函数不能作为构造函数使用,不能使用 new 来实例化,因为它没有自己的 this,而普通函数可以用 new 来创建新的对象。
箭头函数不能使用 yield 关键字来定义生成器函数,而普通函数可以。
箭头函数不支持 call()/apply()函数特性
箭头函数没有 prototype 属性
原型函数不能定义成箭头函数
cookie sessionStorage localStorage 区别
cookie、sessionStorage和localStorage都是存储在浏览器端的客户端存储方式,用于存储一些客户端数据。
区别:
- 生命周期
cookie
的生命周期由 Expires
和 Max-Age
两个属性控制。当设置了 Expires
属性时,cookie
的生命周期为设置的过期时间;当设置了 Max-Age
属性时,cookie
的生命周期为设置的秒数。cookie
在浏览器关闭时也会过期。而 sessionStorage
和 localStorage
的生命周期与浏览器窗口相关,当窗口被关闭时,sessionStorage
数据也会被清空,而 localStorage
数据则会一直存在,直到用户手动删除。
- 存储容量
cookie
的存储容量限制为 4KB
,而 sessionStorage
和 localStorage
的存储容量则较大,可以达到 5MB
或更高。
- 数据共享
cookie
可以被所有同源窗口(指协议、域名、端口相同)访问,而sessionStorage
和localStorage
只能被创建它们的窗口访问。
- 传输方式
cookie
会随着http请求发送到服务器,而sessionStorage
和localStorage
不会发送到服务器,只存在于浏览器端。
- 数据类型
cookie
只能存储字符串类型的数据,而sessionStorage
和localStorage
可以存储除了对象以外的数据类型,如数字、布尔值、数组、甚至是其他复杂的数据结构。但是,它们都可以通过JSON.stringify
和JSON.parse
方法将数据转化为字符串进行存储和读取。
promise.race、promise.all、promise.allSettled 有哪些区别
promise.race
该 Promise 将会在数组中的任意一个 Promise 状态变为 fulfilled
或 rejected
时被解决,且以第一个解决的 Promise 的结果作为其结果返回。
- 用于超时处理,定义一个超时Promise,当超时时取消其他的进行中的promise。
const requestPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 5000));
Promise.race([requestPromise, timeoutPromise])
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
- 监控多个任务,当同时执行多个异步任务,并且只需要其中一个完成就可以继续下一步处理,那么
Promise.race()
就非常适合这个场景。
Promise.all()
Promise.all()
接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态均为 fulfilled
时被解决,并且以数组形式返回所有 Promise 的结果。
如果数组中有任何一个 Promise 被拒绝,则返回的 Promise 将会以最先被拒绝的 Promise 的原因作为其原因拒绝。
Promsie.allSettled()
Promise.allSettled()
接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态都被解决时被解决,并且以数组形式返回所有 Promise 的结果。和 Promise.all()
不同,Promise.allSettled()
不会在有 Promise 被拒绝时拒绝该 Promise。
返回的 Promise 的数组中的每个元素都是一个对象,该对象表示原始 Promise 的结果。每个对象都有一个 status
属性,表示原始 Promise 的状态,其值为字符串 'fulfilled'
或 'rejected'
。如果 Promise 被解决,对象还会包含一个 value
属性,表示 Promise 的解决值。如果 Promise 被拒绝,对象还会包含一个 reason
属性,表示 Promise 的拒绝原因。
使用场景
- 汇总多个异步操作的结果
- 容错处理
- 并发执行,但确保所有操作完成
- 日志记录和审计
Promise.then的第二个参数和 Promise.catch 的区别
Promise.then(onFulfilled, onRejected)
可以同时传递两个回调函数,用来处理 Promise 状态变为 fulfilled
或者 rejected
的情况;而 Promise.catch(onRejected)
则只能用来处理 Promise 状态变为 rejected
的情况,并且使用更加简洁明了
js 判断变量的类型
- typeof 运算符
- instanceof 运算符
- Object.prototype.toString()
- Array.isArray()
Object.prototype.toString() 方法是用来返回当前对象的类型字符串,其实现方式是返回一个类似 "[object Type]" 的字符串,其中 Type 是当前对象的类型。
null 和 undefined 的区别
undefined
是一个变量没有被赋值时的默认值,或者在访问对象属性或数组元素不存在时返回的值。
null
表示一个变量被明确地赋值为没有值。
尝试访问一个已删除的属性将返回undefined
而不是null
。
使用 new 关键字创建对象
在 JavaScript 中,new
关键字用于创建一个对象实例。当使用 new
关键字创建对象时,会发生以下几个步骤:
- 创建一个空的对象。
- 将这个空对象的
[[Prototype]]
属性设置为构造函数的prototype
属性。 - 将这个空对象赋值给构造函数内部的
this
关键字,用于初始化属性和方法。 - 如果构造函数返回一个对象,那么返回这个对象;否则,返回第一步创建的对象实例。
事件循环
js 是单线程的
一段javascript
执行的具体流程就是如下:
- 首先执行宏队列中取出第一个,一段
script
就是相当于一个macrotask
,所以他先会执行同步代码,当遇到例如setTimeout
的时候,就会把这个异步任务推送到宏队列队尾中。 - 当前
macrotask
执行完成以后,就会从微队列中取出位于头部的异步任务进行执行,然后微队列中任务的长度减一。 - 然后继续从微队列中取出任务,直到整个队列中没有任务。如果在执行微队列任务的过程中,又产生了
microtask
,那么会加入整个队列的队尾,也会在当前的周期中执行 - 当微队列的任务为空了,那么就需要执行下一个
macrotask
,执行完成以后再执行微队列,以此反复。 总结下来就是不断从task
队列中按顺序取task
执行,每执行完一个task
都会检查microtask
是否为空,不让过不为空就执行队列中的所有microtask
。然后在取下一个task
以此循环
代码场景
如何检查对象循环引用
使用 WeakSet 特性解决;
// 对 传入的 subject 对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
// 避免无限递归
if (_refs.has(subject)) {
return;
}
fn(subject);
if (typeof subject === "object") {
_refs.add(subject);
for (const key in subject) {
execRecursively(fn, subject[key], _refs);
}
}
}
const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
};
foo.bar.baz = foo; // 循环引用!
execRecursively((obj) => console.log(obj), foo);
ajax 如何获取下载进度
const xhr = new XMLHttpRequest();
xhr.open("GET", "file.url", true);
xhr.responseType = "blob";
xhr.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Downloaded ${percentComplete}%`);
}
};
xhr.onload = function (event) {
// 文件下载完成
const blob = xhr.response;
};
xhr.send();
手写创建 ajax 请求
一般来说,我们可以使用 XMLHttpRequest 对象来创建 Ajax 请求,其流程如下:
- 创建 XMLHttpRequest 对象,通过调用其构造函数来实现。
- 使用 open()方法指定请求的方法、URL 以及是否异步请求。
- 使用 setRequestHeader()方法设置请求头,例如设置请求的 Content-Type。
- 设置响应的回调函数,一般有 onreadystatechange 和 onload 两种方式。
- 使用 send()方法发送请求。
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject) {
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}
var client = new XMLHttpRequest();
//如果是IE的内核ActiveXObject('Microsoft.XMLHTTP');
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
//如果是post请求:client.setRequestHeader('Content-Type','application/X-WWW-form-urlencoded')
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error(' 出错了 ', error);
});
JS 有哪些迭代器
在 JavaScript 中,有三种类型的迭代器:
- Array Iterator(数组迭代器):通过对数组进行迭代以访问其元素。
- String Iterator(字符串迭代器):通过对字符串进行迭代以访问其字符。
- Map Iterator(映射迭代器)和 Set Iterator(集合迭代器):通过对 Map 和 Set 数据结构进行迭代以访问其键和值。
如何使对象 iterable 化, 以其可以支持 for...of 迭代
const myObj = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { done: true };
}
},
};
},
};
for (const item of myObj) {
console.log(item);
}
// 输出:1, 2, 3
js 对象可以使用 for...of 迭代吗
JavaScript 对象本身并不能直接使用 for...of 迭代,因为它并不是一个可迭代对象(iterable)。
但是,如果我们想要遍历对象的属性,可以使用 for...in 循环
const obj = {
name: 'John',
age: 30,
city: 'New York'
};
for (let prop in obj) {
console.log(prop + ': ' + obj[prop]);
}
// 这段代码可以输出:
name: John
age: 30
city: New York
const obj = {
name: 'John',
age: 30,
city: 'New York'
};
// 使用 hasOwnProperty() 方法进行判断对象自身的属性
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
console.log(prop + ': ' + obj[prop]);
}
}
实现一个双向链表
具备添加节点、删除节点、在特定位置插入节点、查找节点、遍历等功能
class Node {
constructor(value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
// 在链表末尾添加节点
push(value) {
const node = new Node(value);
if (this.length === 0) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
this.length++;
return this;
}
// 从链表末尾移除节点
pop() {
if (this.length === 0) {
return undefined;
}
const node = this.tail;
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
this.tail = node.prev;
this.tail.next = null;
node.prev = null;
}
this.length--;
return node.value;
}
// 在链表开头添加节点
unshift(value) {
const node = new Node(value);
if (this.length === 0) {
this.head = node;
this.tail = node;
} else {
this.head.prev = node;
node.next = this.head;
this.head = node;
}
this.length++;
return this;
}
// 从链表开头移除节点
shift() {
if (this.length === 0) {
return undefined;
}
const node = this.head;
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
this.head = node.next;
this.head.prev = null;
node.next = null;
}
this.length--;
return node.value;
}
// 获取指定位置的节点
get(index) {
if (index < 0 || index >= this.length) {
return undefined;
}
let node = null;
if (index < this.length / 2) {
node = this.head;
for (let i = 0; i < index; i++) {
node = node.next;
}
} else {
node = this.tail;
for (let i = this.length - 1; i > index; i--) {
node = node.prev;
}
}
return node;
}
// 在指定位置插入节点
insert(index, value) {
if (index < 0 || index > this.length) {
return false;
}
if (index === 0) {
return !!this.unshift(value);
}
if (index === this.length) {
return !!this.push(value);
}
const node = new Node(value);
const prevNode = this.get(index - 1);
const nextNode = prevNode.next;
prevNode.next = node;
node.prev = prevNode;
node.next = nextNode;
nextNode.prev = node;
this.length++;
return true;
}
// 移除指定位置的节点
remove(index) {
if (index < 0 || index >= this.length) {
return undefined;
}
if (index === 0) {
return this.shift();
}
if (index === this.length - 1) {
return this.pop();
}
const nodeToRemove = this.get(index);
const prevNode = nodeToRemove.prev;
const nextNode = nodeToRemove.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
nodeToRemove.next = null;
nodeToRemove.prev = null;
this.length--;
return nodeToRemove.value;
}
// 反转链表
reverse() {
let node = this.head;
this.head = this.tail;
this.tail = node;
let prevNode = null;
let nextNode = null;
for (let i = 0; i < this.length; i++) {
nextNode = node.next;
node.next = prevNode;
node.prev = nextNode;
prevNode = node;
node = nextNode;
}
return this;
}
// 通过 value 来查询 index
findIndexByValue(value) {
let currentNode = this.head;
let index = 0;
while (currentNode) {
if (currentNode.value === value) {
return index;
}
currentNode = currentNode.next;
index++;
}
return -1; // 如果链表中没有找到该值,返回 -1
}
// 正向遍历链表,并返回遍历结果
forwardTraversal() {
const result = [];
let current = this.head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}
// 反向遍历链表,并返回遍历结果
backwardTraversal() {
const result = [];
let current = this.tail;
while (current) {
result.push(current.value);
current = current.prev;
}
return result;
}
// 循环遍历链表,并返回遍历结果
loopTraversal() {
const result = [];
let current = this.head;
while (current) {
result.push(current.value);
current = current.next;
if (current === this.head) {
break;
}
}
return result;
}
}
js 中继承
组合继承
通过 Object.create 来创建原型中间对象,那么这么来的话,child5 的对象 prototype 获得的是 parent5 父类的原型对象; Object.create 创建的对象,原型对象就是参数;
function Parent5() {
this.name = "parent5";
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = "child5";
}
Child5.prototype = Object.create(Parent5.prototype);
//这个时候虽然隔离了,但是constructor还是只想的Parent5的,因为constructor会一直向上找
Child5.prototype.constructor = Child5;
var s7 = new Child5();
console.log(s7 instanceof Child5, s7 instanceof Parent5);
console.log(s7.constructor);
ES6 中继承
Class 可以通过 extends 关键字实现继承,让子类继承父类的属性和方法。
class Point {
/* ... */
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + " " + super.toString(); // 调用父类的toString()
}
}
手写实现一下 lodash.get
lodash.get 是一个 JavaScript 库 Lodash 中的函数,它允许您在对象中安全地获取深层嵌套的属性值,即使在中间的属性不存在时也不会引发错误。以下是一个简单的实现:
function get(object, path, defaultValue) {
if (!object || !path) {
return defaultValue;
}
const pathArray = path.split(".").filter(Boolean);
let value = object;
for (let i = 0; i < pathArray.length; i++) {
const key = pathArray[i];
value = value[key];
if (value === undefined) {
return defaultValue;
}
}
return value || defaultValue;
}
深拷贝
// 一:只能用于对象内部没有方法时
JSON.parse(JSON.stringify(obj));
// 二: 递归,简陋版本
// 属性值可以是数组或对象,此时进行递归
// 属性值也可以函数
function deepClone(source) {
let target = null;
if (typeof source === "object" && source !== null) {
target = Array.isArray(source) ? [] : {};
for (let [key, value] of Object.entries(source)) {
target[key] = deepClone(value);
}
} else {
target = source;
}
return target;
}
// 但无法解决循环引用的问题
// 例如
let obj = {};
obj.a = obj;
deepClone(obj);
// 会一直递归执行deepClone,造成函数栈溢出
// 复杂版本
// 使用WeakMap解决循环引用的问题
// 使用WeakMap而不是Map是因为其使用的弱引用。该引用不会被垃圾回收器记录。
function deepClone(source, hash = new WeakMap()) {
let target;
if (hash.has(source)) {
return hash.get(source);
}
if (typeof source === "object" && source !== null) {
target = Array.isArray(source) ? [] : {};
hash.set(source, target);
for (let [key, value] of Object.entries(source)) {
target[key] = deepClone(value, hash);
}
} else {
target = source;
}
return target;
}
var obj = {};
obj.a = obj;
deepClone(obj);
使用 lodash
的 cloneDeep
数组去重
使用 ES6 的 Set
// 利用Array.from将Set结构转换成数组
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]); //[1,2,3]
// 拓展运算符(...)内部使用for...of循环
let arr = [1, 2, 3, 3];
let resultArr = [...new Set(arr)];
console.log(resultArr); //[1,2,3]
银行卡号四位空一位
var str = "6222023100014763381";
// 移除空白字符并且每四位增加一个空格
var str = str.replace(/\s/g, "").replace(/(.{4})/g, "$1 ");
console.log(str);
数字字符千分位处理
// 正则
function numToMoneyField(inputString) {
regExpInfo = /(\d{1,3})(?=(\d{3})+(?:$|\.))/g;
var ret = inputString.toString().replace(regExpInfo, "$1,");
return ret;
}
function fun4(num) {
num = (num || 0).toString();
let result = "";
while (num.length > 3) {
result = "," + num.slice(-3) + result;
num = num.slice(0, num.length - 3);
}
if (num) {
result = num + result;
}
return result;
}
防抖
function debounce(func, wait, immediate) {
let time;
let debounced = function () {
let context = this;
if (time) clearTimeout(time);
if (immediate) {
let callNow = !time;
if (callNow) func.apply(context, arguments);
time = setTimeout(
() => {
time = null;
}, //见注解
wait
);
} else {
time = setTimeout(() => {
func.apply(context, arguments);
}, wait);
}
};
return debounced;
}
节流
function throttle(func, wait) {
let time, context;
return function () {
context = this;
if (!time) {
time = setTimeout(function () {
func.apply(context, arguments);
time = null;
}, wait);
}
};
}
合并两个有序数组
function mergeArrays(arr1, arr2) {
const merged = [];
let i = 0; // 第一个数组的指针
let j = 0; // 第二个数组的指针
while (i < arr1.length && j < arr2.length) {
if (arr1[i] <= arr2[j]) {
merged.push(arr1[i]);
i++;
} else {
merged.push(arr2[j]);
j++;
}
}
// 将剩余的未合并元素添加到新数组的末尾
while (i < arr1.length) {
merged.push(arr1[i]);
i++;
}
while (j < arr2.length) {
merged.push(arr2[j]);
j++;
}
return merged;
}
// 示例
const arr1 = [1, 3, 5, 7];
const arr2 = [2, 4, 6, 8];
const mergedArray = mergeArrays(arr1, arr2);
console.log(mergedArray); // 输出 [1, 2, 3, 4, 5, 6, 7, 8]
手写实现 call、apply、bind
function myCall(context) {
// 获取目标函数
var func = arguments[0];
// 将剩余的参数作为目标函数的参数
var args = Array.prototype.slice.call(arguments, 1);
// 使用一个唯一的属性名来避免与对象现有的属性冲突
var uniqueKey = "__myUniqueKey__";
// 将函数临时附加到指定的对象上
context[uniqueKey] = func;
// 在指定的上下文中执行函数,并捕获返回值
var result = context[uniqueKey].apply(context, args);
// 删除临时附加的函数
delete context[uniqueKey];
// 返回函数的执行结果
return result;
}
Function.prototype.myApply = function (context) {
// 获取目标函数
var func = this;
// 检查第二个参数是否为数组或类数组对象
var args =
arguments.length > 1 ? Array.prototype.slice.call(arguments[1]) : [];
// 使用一个唯一的属性名来避免与对象现有的属性冲突
var uniqueKey = "__myUniqueKey__";
// 将函数临时附加到指定的对象上
context[uniqueKey] = func;
// 在指定的上下文中执行函数,并捕获返回值
var result = context[uniqueKey](...args);
// 删除临时附加的函数
delete context[uniqueKey];
// 返回函数的执行结果
return result;
};
Function.prototype.myBind = function bind(context) {
// 获取目标函数
var self = this;
// 获取传递给 bind 的参数
var boundArgs = Array.prototype.slice.call(arguments, 1);
// 创建一个绑定后的函数
var boundFunction = function () {
// 获取调用时传递给绑定后函数的参数
var callArgs = Array.prototype.slice.call(arguments);
// 合并 bind 时的参数和调用时的参数
var finalArgs = boundArgs.concat(callArgs);
// 执行原始函数,并传递合并后的参数
return self.apply(this instanceof self ? this : context, finalArgs);
};
// 如果原函数有 prototype 属性,表明它是一个构造函数
// 我们需要让新的绑定函数继承原函数的 prototype
if (this.prototype) {
var F = function () {};
F.prototype = this.prototype;
boundFunction.prototype = new F();
}
return boundFunction;
};
实现可过期的localStorage
要实现可过期的 localStorage
数据,可以结合使用 localStorage
和 Date
对象。
首先,在存储数据时,需要将数据和过期时间一起存储在 localStorage
中。可以使用 JSON 格式来将数据和过期时间打包存储
function getWithExpiry(key) {
const item = localStorage.getItem(key)
if (!item) return null
const parsedItem = JSON.parse(item)
const now = new Date()
if (now.getTime() > parsedItem.expiry) {
localStorage.removeItem(key)
return null
}
return parsedItem.value
}
深度优先和广度优先
// 递归
function dfsRecursive(node, visited) {
if (!node || visited.has(node)) {
return;
}
visited.add(node);
console.log(node.value);
for (let i = 0; i < node.children.length; i++) {
dfsRecursive(node.children[i], visited);
}
}
// 栈实现
function dfsStack(node) {
const visited = new Set();
const stack = [node];
while (stack.length > 0) {
const current = stack.pop();
if (!current || visited.has(current)) {
continue;
}
visited.add(current);
console.log(current.value);
for (let i = current.children.length - 1; i >= 0; i--) {
stack.push(current.children[i]);
}
}
}
广度优先遍历 BFS
// 队列实现
function bfsQueue(node) {
const visited = new Set();
const queue = [node];
while (queue.length > 0) {
const current = queue.shift();
if (!current || visited.has(current)) {
continue;
}
visited.add(current);
console.log(current.value);
for (let i = 0; i < current.children.length; i++) {
queue.push(current.children[i]);
}
}
}