背景介绍:
从研一刚开始找实习到现在秋招,这一路经历了不少八股拷打,经常被要求手撕一些js基础题,每次面试完后不语,只是默默打开笔记,把被问到的八股/手撕自己整理,方便日后复习。因此,记录了很多手撕题,在此做个分享,有误之处欢迎讨论指正。
下面的几乎每道题都是笔者被大厂问到过的,都是些基础的题目,基础不牢地动山摇,书到用时方恨少啊~。切忌走马观花,务必深刻理解烂熟于心。建议以本文为大纲,自行拓展广度和深度。
面试感悟:
手撕题应该理解原理,练习/默写3遍及以上,确保能立即写出来。
基础八股在回答时,一个是要说话条理清晰,第二个是回答全面。
基本数据类型
7种原始类型:String
, Number
, Boolean
, Null
, Undefined
, Symbol
, BigInt
引用类型:Object
拓展 - TS的数据类型
基本类型(如 string
, number
, boolean
,undefined
,symbol
,bigint
)
复合类型(如 array
, tuple
, object
,enum
)
特殊类型(如 any
,unknown
, void
,never
)
类型判断的几种方式?
typeof
typeof
判断基础类型和函数,但不能区分对象类型(不能区分Object和Array)。
注意typeof
的两个槽点:typeof null === 'object'
和 typeof NaN === 'number'
instanceof
a instanceof A
用来判断某个实例是否是某个构造函数创造出来的。原理是:这个实例的原型链上,有没有这个构造函数的原型。
Object.prototype.toString.call()
Object 原型上的toString方法被xx调用,用来判断xx的类型。
//判断基础类型
Object.prototype.toString.call(123); // "[object Number]"
//判断数组
Object.prototype.toString.call([]); //"[object Array]"
//判断其他内置类型
Object.prototype.toString.call(new Map()); //"[object Map]"
Object.prototype.toString.call(/abc/); // "[object RegExp]"
Array.isArray()
用来判断是否为数组。
判断数组的3中方式:
Array.isArray(arr)
arr instanceof Array
Object.prototype.toString.call(arg) === '[object Array]'
你用过Symbol和BigInt吗?
Symbol
Symbol是一种ES6引入的、表示独一无二值的第七种基本数据类型,主要用于避免命名冲突,作为对象属性的标识符,以实现唯一标识和支持可定制的属性
特性:使用Symbol用同样字符串构造产生的变量,是不全等的。
let k1 = Symbol("KK");
console.log(k1); // Symbol(KK)
typeof(k1); // "symbol"// 相同参数 Symbol() 返回的值不相等
let k2 = Symbol("KK");
k1 === k2; // false
使用场景举例:
1.vue中创建provide/inject使用的key
//创建不同key
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')//父组件
<script setup>
import { provide, ref } from 'vue'
import { ThemeKey, UserKey, UpdateUserKey } from './keys.js'const theme = ref('dark')
const user = ref({ name: 'Alice', age: 25 })provide(ThemeKey, theme)
provide(UserKey, user)
</script>//子组件
<script setup>
import { inject } from 'vue'
import { ThemeKey, UserKey } from './keys.js'const theme = inject(ThemeKey)
const user = inject(UserKey)
</script>
2.apply/call函数的实现中,为了防止污染属性
Bigint
能创建任意大的整数,避免了Number的最大整数限制(2^53-1)
1.创建BigInt::在数字后面添加 n
后缀即可创建BigInt。也可以使用 BigInt() 函数。
2.支持两个BigInt之间的加减乘除、取模% 和 指数**运算
const a = 10n;
const b = 5n;
console.log(a + b); // 15n
console.log(a * b); // 50n
Map和WeakMap的区别?
Map的key可以是任意值,当key为对象时,如果作为key的对象没有被引用,Map的key不会被回收。
WeakMap的key只能是对象(Object/Array/Function)和Symbol,当key为对象时,如果作为key的对象没有被引用,WeakMap的key会被回收——垃圾回收机制会回收该键值对。
Map和普通对象区别?
1.key的顺序
Map保留了key-value插入的顺序。
普通字面对象遍历顺序没有保证。
2.key的类型
Map的key可以是任意类型(String, Boolean, Number, Symbol等)
普通字面对象的key只能是 String和Symbol(注意:当使用数字做key,实际上是给转成了字符串处理);
3.迭代方式
Map本身是可迭代对象,可以被for of
迭代。
普通字面对象本身不可迭代,需要借助Object.keys()
来迭代(不包含原型上的key)。(注意:使用for in
可以遍历普通字面对象的所有key,这个key包含了来自原型上的key)
4.序列化
普通字面对象可以被JSON.stringify()
序列化
Map无法被JSON.stringify()
序列化
5.性能考虑
在涉及频繁增删键值对的场景下,Map 的性能通常优于普通对象
拓展 - for of
可以迭代可迭代对象的原理,这个就写在后面了~
浮点数精度 0.1+0.2!=0.3 问题
0.1+0.2 = 0.30000000000000004
。原因解释:这是所有采用IEEE 754标准的浮点数都存在的问题,0.1和0.2在进行计算的时候要转为二进制进行计算,但是0.1和0.2的二进制是无限循环的,那么当计算(加)完成后的数也是无限循环小数,有效位存不下就会“截断”。所以计算结果是一个近似值(会存在一点点误差)。
解决方法1
toFixed
(参数表示小数位数)toPrecision
(参数表示有效数位数)
console.log((0.1 + 0.2).toFixed(1)); // 0.3
console.log((0.1 + 0.2).toPrecision(1)); // 0.3
解决方法2
使用mathjs等第三方库,
解决方法3
不使用浮点数,转换成整数计算(用整数表示浮点数)。比如1块4分,比起1.04,可直接使用104。
手撕
深拷贝
1.JSON.stringify
深拷贝存在的问题
- 序列化会丢失:undefined,symbol,function这些会被忽略
- 序列化过程中Date会变成字符串,RegExp会变成空{}
- 无法处理原型链 (原型链丢失)
- 无法处理循环引用(报错)
循环引用问题举例:
const a = {next: null
}
const b = {next = a;
}
a.next = b;
Ps.后面会有个JSON.stringify
的实现(手撕题)哦~
2.深拷贝基础版 - 考虑基础类型、数组、对象和对象的原型链
/*按基础类型、数组、内置对象、对象 4块分类处理基础类型 - 直接返回数组 - 遍历deepCopy内置对象 - 可迭代的就迭代并deepCopy,Date和RegExp就重新new一个对象 - 创建一个空的新对象(创建时带原型),遍历对象的所有属性并deepCopy,依次挂到新对象上
*/
function deepCopy(obj){const type = Object.prototype.toString.call(obj) // '[object String]','[object Object]'...const isPrimitive = /String|Number|Boolean|Null|Undefined|Symbol|BigInt|Function/.test(type)// 基础类型if(isPrimitive){return obj}else if(type === '[object Array]'){ //数组return obj.map(item=>deepCopy(item))}else if(type === '[object Map]'){const map = new Map()for(const [k, v] of obj.entries()){map.set(k, deepCopy(v))}return map}else if(type === '[object WeakMap]'){const map = new WeakMap()for(const [k, v] of obj.entries()){map.set(k, deepCopy(v))}return map}else if(type === '[object Set]' ){const set = new Set()for(const item of obj){set.add(deepCopy(item))}return set}else if(type === '[object WeakSet]'){const set = new WeakSet()for(const item of obj){set.add(deepCopy(item))}return set}else if(type === '[object Date]'){return new Date(obj)}else if(type === '[object RegExp]'){return new RegExp(obj)}else{ //对象const ans = Object.create(obj.__proto__) // 考虑原型,以obj.__proto__创建一个新对象for(const key of Object.keys(obj)){ // for in会把原型上的属性直接拷贝过来,所以用keys()ans[key] = deepCopy(obj[key])}return ans;} }
测试:
const obj = {name: '疯狂踩坑人',children: [{name: 'God',children: [{name: 'Jessie'}]},],say : function(){console.log('my name is '+this.name);},skills: ["CET-6", "Coding"],relationship:{parent: Symbol('parent'),brothers: Symbol('brothers')},
}
console.log(deepCopy(obj))
3.深拷贝高级版 - 考虑循环引用
/*例. a = {next: a} 用WeakMap记录上层出现过的对象,当某个属性引用到前面出现的对象,说明出现了循环引用,直接从WeakMap返回该对象即可。
*/
function advanceDeepCopy(target){const wkMap = new WeakMap()function _deepCopy(obj){const type = Object.prototype.toString.call(obj) const isPrimitive = /String|Number|Boolean|Null|Undefined|Symbol|BigInt|Function/.test(type)// 基础类型if(isPrimitive){return obj}else if(type === '[object Array]'){ //数组obj.map(item=>_deepCopy(item))}else if(type === '[object Map]'){const map = new Map()for(const [k, v] of obj.entries()){map.set(k, _deepCopy(v))}return map}else if(type === '[object WeakMap]'){const map = new WeakMap()for(const [k, v] of obj.entries()){map.set(k, _deepCopy(v))}return map}else if(type === '[object Set]' ){const set = new Set()for(const item of obj){set.add(_deepCopy(item))}return set}else if(type === '[object WeakSet]'){const set = new WeakSet()for(const item of obj){set.add(_deepCopy(item))}return set}else if(type === '[object Date]'){return new Date(obj)}else if(type === '[object RegExp]'){return new RegExp(obj)}else{ //对象。用weakMap记录对象,如果后续要拷贝同一对象,则不要深拷贝,而是直接反返回记录的这个对象if(wkMap.has(obj)){ return wkMap.get(obj)}const ans = Object.create(obj.__proto__) // 考虑原型wkMap.set(obj, ans)for(const key of Object.keys(obj)){ ans[key] = _deepCopy(obj[key])}return ans;} }return _deepCopy(target)
}
判断是否为空对象
可以使用lodash中的isEmpty方法判断,那么如何实现isEmpty呢?
方式一:stringify
. 存在的小问题是无法处理函数和循环引用
return !(JSON.stringify(obj) === '{}')
方式二:使用for ... in
for(let key in obj){return true;
}
return false;
方式三:使用Object.keys
(Date/RegExp/空Map/空Set/空数组 这些内置对象通过Object.keys
获取到都是一个空数组)
return !Object.keys(obj).length
判断是否为空对象,除了判断普通对象外,还需要判断null、内置对象这些情况
function isEmpty(obj){if(typeof obj === 'object' && obj !== null){return !Object.keys(obj).length}return false;
}//测试
console.log(isEmpty({})) // true
console.log(isEmpty({ name:'疯狂踩坑人' })) // false
console.log(isEmpty([])) // true
console.log(isEmpty([1,2])) // false
console.log(isEmpty(new Date())) //true
原型和原型链
原型链
我觉得这篇文章 一文彻底搞懂原型链很清晰的解释了原型链的,可以仔细看看。
这里借用了一下这篇文章的图,如下:
原型链关系对于刚接触的同学会有点绕,所以一定要先建立先验知识,记住下面两点:
- 记住第1点:原型是一个普通对象(是普通对象,就有
__protop__
指针) - 记住第2点:函数和普通对象上(默认/自动)都挂载了一个原型(函数通过
prototype
指针指向,对象通过私有属性__proto__
指向)
如果普通对象是通过某个(构造)函数创建来的,那么它们的原型是同一个。普通对象通过__proto__
指向原型,刚才说了原型本身也是一个普通对象,有__proto__
,这样就构成了原型链。原型链就是通过__proto__
一级级往上找原型形成的链。
下面举一个例子来理解:
function A(name){this.name = name
}
const a = new A('疯狂踩坑人');
默认/自动/没有手动修改的情况下,A.prototype
(A的原型)是一个Object构建出来的对象,所以此时a的原型链如下:
a.__proto__ 指向 {..., __proto__指向{..., __proto__:null} }
打印结果如图:
继承
下面将重点介绍4种方式:借用构造函数继承
、原型式继承
、组合式继承
和寄生式组合式继承
。
还有另外两种(原型式继承
, 寄生式继承
)就不贴代码了,自行了解下即可。
1.借用构造函数继承
:在子构造方法中调用父构造方法,完成this的属性方法绑定。
/*1.构造函数继承*/
function test1(){function Foo(label){this.labels = [label]this.say = function(){console.log('my name is '+this.name+', a '+this.label+'.');}}function Bar(name, label){ Foo.call(this, label)this.name = name}const b1 = new Bar('疯狂踩坑人', '程序员')b1.say() // my name is 疯狂踩坑人, labels: 程序员.
}
- 优点:创建子类对象可以传参,每个子类实例保留自己独立的属性
- 缺点:方法不能共享(每个实例都有一个独立的say方法)
2.原型链继承
:通过原型链,沿用祖先的属性和方法。
/*2.原型链继承*/
function test2(){function Foo(label){this.labels = [label]}Foo.prototype.say = function(){console.log('my name is '+this.name+', labels: '+String(this.labels)+'.');}function Bar(name){ this.name = name;}Bar.prototype = new Foo('自由职业');const b1 = new Bar('疯狂踩坑人'); b1.labels.push('程序员') const b2 = new Bar('疯狂踩坑人2');b2.labels.push('程序员2') //b1.say() // my name is 疯狂踩坑人, labels: 自由职业,程序员,程序员2.b2.say() // my name is 疯狂踩坑人2, labels: 自由职业,程序员,程序员2.
}
- 优点:方法共享,不用每个对象都创建一个方法
- 缺点:属性共享,不能在创建子类实例时给父类传参。(后续操作继承的属性时,会影响到所有实例)
3.组合式继承
:结合了前面两项的优点
把实例方法都放在原型对象上,通过Child.prototype = new Father()
,以实现函数复用。
通过Foo.call(this)
继承父类的属性,并保留能传参的优点.
/* 组合式继承 */
function test3(){function Foo(label){this.labels = [label]}Foo.prototype.say = function(){console.log('my name is '+this.name+', labels: '+String(this.labels)+'.');}function Bar(name, label){ Foo.call(this, label)this.name = name;}Bar.prototype = new Foo();const b1 = new Bar('疯狂踩坑人', '自由职业'); b1.labels.push('程序员') const b2 = new Bar('疯狂踩坑人2', '自由职业2');b2.labels.push('程序员2') //b1.say() // my name is 疯狂踩坑人, labels: 自由职业,程序员.b2.say() // my name is 疯狂踩坑人2, labels: 自由职业2,程序员2.
}
- 优点:属性不共享,相互独立,可以通过传参来初始化。函数复用/共享。
- 缺点:直接用父类构造对象作为原型,这个原型是有一些不必要的属性(浪费内存)。上述代码中就是,子类实例本身有
this.labels
,子类的原型上也有labels
4.寄生式组合式继承
:主要在组合式的基础上做一个小改动——原型不使用new Foo()
,而是使用Object.create(Foo.prototype)
function test4(){// 属性放构造方法里,方法放原型上function Foo(label){this.labels = [label]}Foo.prototype.say = function(){console.log('my name is '+this.name+', labels: '+String(this.labels)+'.');}function Bar(name, label){ Foo.call(this, label)this.name = name;}Bar.prototype = Object.create(Foo.prototype, {constructor:{value: Bar}})// 这样一来,Bar.prototype = {__proto__: Foo.prototype}const b1 = new Bar('疯狂踩坑人', '自由职业'); b1.labels.push('程序员') const b2 = new Bar('疯狂踩坑人2', '自由职业2');b2.labels.push('程序员2') //b1.say() // my name is 疯狂踩坑人, labels: 自由职业,程序员.b2.say() // my name is 疯狂踩坑人2, labels: 自由职业2,程序员2.
}
这是最完美的继承方案。具备了组合式继承
的属性不共享、方法共享的优点,同时解决了它的缺点,令子类的原型上没有多余的属性。
手撕
instanceof
a instanceof A
用来判断某个实例是否是某个构造函数创造出来的。原理是:这个实例的原型链上,有没有这个构造函数的原型。
function instanceOf(obj, constructor){//obj的原型链上,是否存在constructor.prototypelet proto = obj.__proto__while(proto){if(proto === constructor.prototype){return true;}proto = proto.__proto__}return false
}
测试:
function A(a){this.a = a
}
function B(b){this.b = b
}
// B继承A
B.prototype = Object.create(B.prototype, {constructor:{value: B}
})
const x = new B('hello')console.log(x instanceof A); //true
console.log(instanceOf(x, A)); //true
作用域和作用域链
作用域链是指在JavaScript中,当访问一个变量时,解释器会按照从内到外的顺序查找变量的机制,这个查找的路径就构成了一个链条。这个链条由当前的作用域开始,然后逐级向上查找,直到全局作用域。
在var变量中,只有functon作用域和全局作用域。下面通过例子来说明作用域链:
var x = "global scope";
function checkscope(){var x = "local scope";console.log(x); // "local scope"
}
// 在checkscope函数中,先从checkscope函数域中找x变量,发现存在x的声明,就使用函数域的x,不会找到外面的全局x.
var x = "global scope";
function checkscope(){console.log(x); // "global scope"
}
// 在checkscope函数中,先从checkscope函数域中找x变量,发现没有x声明,然后从外面第一层(这里是全局作用域)找到了x的声明,就使用了外面的全局x
var, let, const 区别
var和function存在变量提升,以var举例:
function funcTest() { console.log(arg); var arg = 2;
}
funcTest();
等价于
function funcTest() { var arg; // 变量声明被提升到函数作用域顶部,初始值为 undefined console.log(arg); // 因此这里输出 undefined arg = 2; // 赋值操作保留在原位执行
}
这样一来var的变量可以先使用,再声明。对于function关键字定义的函数也是如此。
var和let区别一
- var只有funciton作用域和全局作用域
- let则除了funciton作用域和全局作用域,还有块级(
{}
)作用域
var和let区别二
- var有变量提升,可以先使用后声明。
- let/const也有变量提升,但是由于
暂时性死区
,先使用后声明会导致报错。
作用域考题一:
// 题1
for(var i=0; i<5; i++){setTimeout(()=>{console.log(i);}, 1000)
}
// 5 5 5 5 5// 每次迭代,都是一个新的块作用域,每个块作用域都有一个独立的i变量
for(let i=0; i<5; i++){setTimeout(()=>{console.log(i);}, 1000)
}
// 0 1 2 3 4
作用域考题二:
// 题2
var a = 1;
(() => {console.log(a);a = 2;
})();
// 输出 1,查找到全局 a var a = 1;
(() => {console.log(a);var a = 2;
})();
// 输出 undefined,变量声明提升 var a = 1;
(() => {console.log(a);let a = 2;
})();
// 输出 报错
闭包
闭包是什么?
一句话说明:闭包是一种编程形式,通过内部函数访问外部函数的变量,从而做到变量复用和避免污染全局变量等作用。 (也可以简单的把这个内部函数称为闭包,一种共识的表示而已)
闭包需要满足的条件:
- 函数嵌套,外面函数内部定义了一个内部函数
- 将内部函数返回
闭包举例:
function outer() {const x = 10;function inner() { console.log('inner: ', x); } return innner;
} const fn = outer();
fn(); // inner: 10
下面举一个反面例子,不少人会误以为这也是一种闭包:
function outer(callback) {const x = 10;callback(x);
} function inner() { console.log('callback: ', x);
} outer(inner); // callback: 10
这种通过参数将函数传入的方式,闭包的两个条件其实都不满足。其实可以从本质上分析为什么上面这种不是闭包。
首先,关于这个问题还可以进一步深入解释,从词法环境(执行上下文)和垃圾回收的角度去分析。时间充足且有兴趣的同学可以看:
理解 JavaScript 中的执行上下文和执行栈
变量作用域,闭包
然后,这里简单分析下。
第一个例子 | 第二个例子 |
---|---|
当执行outer函数时会创建一个词法环境,执行栈将x、inner装入。由于inner函数(闭包)被引用,导致执行栈中的x、inner变量都不会被清理,内存并没有释放。 | 当执行outer函数时会创建一个词法环境,执行栈将x、inner装入。当outer函数结束,outer的词法环境(执行上下文)会被销毁,执行栈中的变量会被清理。 |
手撕柯里化
一句话说明:把一个接收多个参数的函数,转换为一系列接收单个参数的函数,直到参数全部收集才执行计算。
记住两个关键词:收集参数、延迟计算。
下面通过一个例子来认识下柯里化:
编写一个像 sum(a)(b) = a+b
这样工作的 sum
函数。
function sum(a, b){return a+b;
}function currySum(sum){return function(a){return function(b){return a+b}}
}
这个currySum
函数就被称为"柯里化函数",它返回的函数称为"被柯里化的函数"。看完这个例子,相信聪明的同学看出来了,一个通用型的柯里化函数是应该通过递归来实现的。下面就来手撕一个通用型的柯里化函数吧
实现一个通用的curry函数:
function curry(func, initArgs){const arity = func.length; // func函数的形参数量initArgs = initArgs || []return function(...args){const accArgs = initArgs.concat(args)if(accArgs.length < arity){return curry(func, accArgs)}else {return func.apply(this, accArgs) //谁调用了包装器,这个func就被谁调用}}
}
测试:
function log(date, importance, info){console.log(`${date} : ${importance} : ${info}`)
}
// 测试
const curriedLog = curry(log)const logTool = curriedLog(new Date())
logTool('error', '出错了!')
logTool('warn', '这是警告')
logTool('info', '正常输出')
this指向
全局的this(浏览器环境): 1.undefined(严格模式); 2.window(非严格模式)
全局的this(node环境):1.严格模式,undefined; 2.非严格模式, 指向模块自身的导出对象 module.exports
函数的this:
- 普通函数作为对象的方法调用,this指向该对象
- 构造函数调用(new),this指向实例对象
- 全局定义的函数会自动成为window的方法,直接调用相当于window调用,this是window
- 非全局定义的普通函数,通过赋值给全局变量,再调用,this是window
- 箭头函数没有自己的this,指向定义时所在this域
手撕
有三种方法可以改变函数this的指向,需要对这三种方法随时能手撕出来。
方法 | 调用时机 | 参数形式 | 返回值 |
---|---|---|---|
call |
立即执行 | 参数列表 (arg1, arg2, ... ) |
原函数的执行结果 |
apply |
立即执行 | 参数数组 ([arg1, arg2, ...] ) |
原函数的执行结果 |
bind |
不立即执行,返回新函数 | 参数列表 | 一个永久绑定 this 的新函数 |
apply
Function.prototype.apply = function(context, args){const fn = Symbol('fn')context[fn] = thisconst res = context[fn](...args)delete context[fn]return res;
}
call
Function.prototype.call = function(context, ...args){const fn = Symbol('fn')context[fn] = thisconst res = context[fn](...args)delete context[fn]return res;
}
bind
// 显式绑定,不管谁调用,函数最终的this绑定的是context
Function.prototype.bind = function(context){context = context || windowconst fn = Symbol('fn')context[fn] = thisreturn function (){// 不是直接指向this()// 而是改变this的指向,再执行return context[fn](...arguments)}
}
根据实现代码,提一个小问题:为什么需要用Symbol ?
因为要把函数this
挂载到上下文context
对象上,但不能污染上下文对象原来的属性,所以用Symbol。
函数
箭头函数和普通函数的区别
箭头函数和普通函数的区别:
- 箭头函数没有this,不能改变this指向 (即无法通过call, apply, bind去显示的改变this)
- 箭头函数没有arguments
- 箭头函数不能作为构造函数 (不能new)
- 箭头函数没有原型
动态函数
API new Function ([arg1[, arg2[, ...argN]],] functionBody)
例子:
// 创建一个加法函数。最后一个参数字符串,可以被当做js执行
const add = new Function('a', 'b', 'return a + b');
console.log(add(2, 3)); // 输出: 5
new实现
new的过程(5步):
- 创建一个空对象,作为将要返回的对象实例
- 将这个空对象的原型,指向了构造函数的
prototype
属性 - 将这个空对象赋值给函数内部的
this
关键字 - 开始执行构造函数内部的代码
- 如果构造函数返回一个对象,那么就直接返回该对象,否则返回创建的对象
通过伪代码代码记忆:
obj = Object.create(Constructor.prototype) //1,2
const res = Constructor.apply(obj, arguments) //3,4
return typeof res === 'object' ? res : obj; //5
数组
数组有哪些方法?
方法分类 | 方法名称 | 描述 | 返回值 | 是否改变原数组 | ES版本 |
---|---|---|---|---|---|
添加/删除元素 | push() |
向数组末尾添加一个或多个元素 | 新数组的长度 | 是 | ES5 |
pop() |
删除并返回数组的最后一个元素 | 被删除的元素 | 是 | ES5 | |
unshift() |
向数组开头添加一个或多个元素 | 新数组的长度 | 是 | ES5 | |
shift() |
删除并返回数组的第一个元素 | 被删除的元素 | 是 | ES5 | |
数组截取/拼接 | slice() |
返回数组的指定部分(浅拷贝) | 新数组 | 否 | ES5 |
splice() |
在指定位置删除/添加元素 | 被删除元素组成的数组 | 是 | ES5 | |
concat() |
合并两个或多个数组 | 合并后的新数组 | 否 | ES5 | |
数组转换 | join() |
将数组元素连接成字符串 | 字符串 | 否 | ES5 |
toString() |
将数组转换为字符串 | 字符串 | 否 | ES5 | |
Array.from() |
将类数组对象转换为数组 | 新数组 | 否 | ES6 | |
排序/反转 | sort() |
对数组元素进行排序 | 排序后的数组 | 是 | ES5 |
reverse() |
反转数组元素的顺序 | 反转后的数组 | 是 | ES5 | |
查找/判断 | indexOf() |
查找元素第一次出现的索引 | 索引值(未找到返回-1) | 否 | ES5 |
lastIndexOf() |
查找元素最后一次出现的索引 | 索引值(未找到返回-1) | 否 | ES5 | |
includes() |
判断数组是否包含某个元素 | 布尔值 | 否 | ES6 | |
find() |
查找第一个符合条件的元素 | 元素值(未找到返回undefined) | 否 | ES6 | |
findIndex() |
查找第一个符合条件的元素索引 | 索引值(未找到返回-1) | 否 | ES6 | |
findLast() |
查找最后一个符合条件的元素 | 元素值(未找到返回undefined) | 否 | ES2023 | |
findLastIndex() |
查找最后一个符合条件的元素索引 | 索引值(未找到返回-1) | 否 | ES2023 | |
遍历/迭代 | forEach() |
遍历数组执行回调函数 | undefined | 否 | ES5 |
map() |
对每个元素执行函数并返回新数组 | 新数组 | 否 | ES5 | |
filter() |
过滤符合条件的元素组成新数组 | 新数组 | 否 | ES5 | |
reduce() |
从左到右累加数组元素 | 累加结果 | 否 | ES5 | |
reduceRight() |
从右到左累加数组元素 | 累加结果 | 否 | ES5 | |
其他操作 | every() |
检测所有元素是否都满足条件 | 布尔值 | 否 | ES5 |
some() |
检测是否有元素满足条件 | 布尔值 | 否 | ES5 | |
flat() |
将嵌套数组扁平化 | 新数组 | 否 | ES6 | |
flatMap() |
先map后扁平化(深度为1) | 新数组 | 否 | ES6 | |
fill() |
用固定值填充数组元素 | 填充后的数组 | 是 | ES6 |
数组去重的方法?
方式一、使用Set
function unique(arr){Array.from(Set(arr))
}
方式二、for双重循环
function unique(arr){const uniqueArr = [];for (let i = 0; i < arr.length; i++) {let isUnique = true;// 检查当前元素是否已在结果数组中for (let j = 0; j < uniqueArr.length; j++) {if (arr[i] === uniqueArr[j]) {isUnique = false;break;}}if (isUnique) {uniqueArr.push(arr[i]);}}return uniqueArr;
}
方式三、使用filter嵌套循环
function unique(arr){return arr.filter((item, index) => arr.indexOf(item) === index);
}
方式1的时间复杂度O(n),另外两个时间复杂度是O(n^2). (Set的插入和查找时间复杂度是O(1),所以能做到第一种方式时间复杂度O(n).)
迭代方式 for in 和 for of的区别?
for of 能迭代可迭代对象的原理
JavaScript 中许多内置类型默认就是可迭代的,因为它们都实现了 [Symbol.iterator]
方法,例如数组、字符串、Map 和 Set。这也是你能直接用 for...of
循环它们的原因。
看定义:
type IteratorFn = () => { next(): { value: any; done: boolean };
}; type Iterator = { [Symbol.iterator]: IteratorFn;
};
就是可迭代对象
有一个属性[Symbol.iterator]
,它表示一个函数,这个函数返回一个带有next
方法的对象。
不知道你见没见过Map.next()
,这说明了可迭代对象
上可以通过不断调用next方法来遍历。
基于这一点,你可以创建一个对象来实现这种协议,从而变成可迭代的,比如:
const myIterable: Iterator = {[Symbol.iterator]: () => {let count = 0;return {next: () => {if (count < 3) {return { value: count++, done: false };}return { value: undefined, done: true };}};}
};
for in 工作原理
for in是用于遍历对象属性的一种循环机制。它可以遍历对象上除了Symbol外的所有可枚举属性,包括继承的可枚举属性。
// 创建一个对象并设置其原型
const proto = { inheritedProp: '来自原型' };
const obj = Object.create(proto);
obj.ownProp = '自身属性';// 使用 for...in 遍历(会遍历自身和原型链上的可枚举属性)
for (let key in obj) {console.log(key); // 输出: ownProp, inheritedProp
}```由于会找原型链上的属性,所以性能不高。通常使用`for of` 遍历`Object.keys()`来替代。#### 数组乱序的方法
核心思想:遍历数组的每个位置,从该位置后面的元素中随机选择一个和该位置元素交换。```jsfor (var i = 0; i < arr.length; i++) {const randomIndex = Math.floor(Math.random() * (arr.length - i)) + i;[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}//测试
var arr = [1,2,3,4,5,6,7,8,9,10];
console.log(arr);
随机获取数组的一个元素索引:
const randomIndex = Math.floor(Math.random() * arr.length)
随机获取数组某区间[i,j]
的索引:
const randomIndex = Math.floor(Math.random() * (j-i+1)) + i
手撕
reduce
Array.prototype.myReduce = function(fn, initialValue) {var arr = Array.prototype.slice.call(this);var res, startIndex;res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项startIndex = initialValue ? 0 : 1;for(var i = startIndex; i < arr.length; i++) {// 把初始值、当前值、索引、当前数组 传递给调用函数res = fn.call(null, res, arr[i], i, this); }return res;
}// 测试
const res = [1,2,3].myReduce((pre, cur)=>{return pre + cur;
})
console.log(res) // 6
flat
遍历数组,如果元素是数组,那么递归继续「打平」,返回一个新数组。
function flat(arr, n=1){if(n<1){return arr; //n<1后不打平了}let res = []for(const item of arr){if(Array.isArray(item)){res = res.concat(flat(item, n-1));}else{res.push(item)}}return res;
}// 测试
const arr = [1,2,['ss','hh', ['peace', 'love', null, {name: '疯狂踩坑人'}]], 3]
console.log(flat(arr, 1)); // [ 1, 2, 'ss', 'hh', [ 'peace', 'love', null, { name: '疯狂踩坑人' } ], 3 ]
异步Promise
宏任务&微任务以及事件循环
这篇文章由浅入深了解浏览器的事件循环Event Loop可谓是比较详细介绍了事件循环。这里总结下就是:
- 浏览器/JS引擎中有两个队列:宏任务队列、微任务队列,分别用来存放浏览器的两类异步任务——宏任务和微任务。
- 所谓的宏任务和微任务就是一段延后执行的脚本/回调函数,比如
setTimeout(fn, 5)
这里的fn将是一个宏任务。 - 浏览器先从微任务队列中取出所有任务执行完,然后从宏任务队列中取出一个宏任务执行。之后总是这样循环:先取出所有微任务,再取一个宏任务执行。构成了事件循环。
下面列举了宏任务和微任务:
宏任务
# | 任务类型 | 浏览器 | Node |
---|---|---|---|
1 | I/O (请求,读写文件) | ✅ | ✅ |
2 | setTimeout | ✅ | ✅ |
3 | setInterval | ✅ | ✅ |
4 | setImmediate | ❌ | ✅ |
微任务
# | 任务类型 | 浏览器 | Node |
---|---|---|---|
1 | process.nextTick | ❌ | ✅ |
2 | MutationObserver | ✅ | ❌ |
3 | IntersectionObserver | ✅ | ❌ |
4 | Promise.then/catch/finally | ✅ | ✅ |
Promise规范和原理
请你简单介绍下Promise?
1.介绍创建:Promise初始化接受一个函数参数,该函数会立即执行。这个参数函数接受两个参数:resolve函数、reject函数。Promise对象有3个状态——pending
,fulfilled
和rejected
,resolve能将状态变成fulfilled
,reject能将状态变成rejected
。一旦状态从pending
变成结果状态,状态就不再改变。
2.介绍then/catch方法:当Promise对象状态变成结果状态,就会调用then的回调方法,then函数接受两个函数参数:成功回调&失败回调;并且返回一个新的Promise,这使得Promise可以链式调用。then返回的这个新的Promise的结果状态取决于上一个Promise的状态和then的两个回调函数的处理。catch方法可以作为错误的兜底处理。
3.介绍静态方法:Promise.resolve
, Promise.reject
, Promsie.all
, Promise.race
, Promise.allSettled
建议时间充足的情况下,都尝试自己实现一个简单的Promise,理解其运行原理,可以参考「硬核JS」图解Promise迷惑行为|运行机制补充或其他资料。
异步输出练习
async function async1() {console.log('async1 start');await async2();console.log('async1 end');
}
async function async2() {console.log('async2');
}
console.log('script start');
setTimeout(function() {console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {console.log('promise1');resolve();
}).then(function() {console.log('promise2');
});
console.log('script end');/*
过程分析:
宏任务队列:setTimeout
微任务队列:async1 end, promise2输出 =======================
script start
async1 start
async2
promise1
script end (到这里,整个脚本结束。下面就是先取所有微任务,再取一个宏任务)
async1 end
promise2
setTimeout
*/
手撕
实现delay
实现一个delay函数,等待ms时间后继续执行后面代码。
function delay(ms){return new Promise((resolve)=>{setTimeout(()=>{resolve()}, ms)})
}async function test(){await delay(2000)console.log('print after ms')
}
实现promise的all/race/allSettled
实现all
all
接受一个promise数组/可迭代对象,返回一个promise。当数组的所有promise都成功,则结果fulfilled
;任一个失败则结果是rejected
Promise.all([pa, pb]).then([resA, resB]=>{// 全部成功
}).catch(e=>{// 任一个失败
})
具体实现:
Promise.all = (iterable) => {return new Promise((resolve, reject) => {// 处理空迭代对象的情况if (!iterable || iterable.length === 0 || iterable.size === 0) {return resolve([]);}const len = Array.isArray(iterable) ? iterable.length : iterable.size; // 数组是length, Map是sizeconst results = Array(len).fill(null);let successCnt = 0;// 处理每个promiseiterable.forEach((item, i) => {Promise.resolve(item).then((res) => {results[i] = res;successCnt++;if (successCnt === len) {resolve(results);}}).catch(e => {reject(e);});});});
}
这里注意,需要用Promise.resolve
包裹item,因为item可能不是Promise。(Promise.resolve(item)
在item是Promise的时候直接返回item,否则会返回一个内部创建的成功Promise,value是item)
实现race
race
接受一个promise数组/可迭代对象,返回一个promise。当数组中任一个promise先成功,则结果fulfilled
;任一个先失败则结果是rejected
Promise.race([pa, pb]).then(res=>{// 任一个先成功
}).catch(e=>{// 任一个先失败
})
具体实现:
Promise.race = (iterable)=>{if(!iterable || iterable.length === 0 || iterable.size === 0){return Promise.reject(new TypeError('Promise.race requires an iterable argument'))}// 重点return new Promise((resolve, reject)=>{iterable.forEach(item=>{Promise.resolve(item).then((res)=>{resolve(res)}).catch(e=>{reject(e)})})})
}
实现allSettled
MDN allSettled规范
allSettled规范:
- 当参数数组中所有Promise都达到结束状态时才结束promise,并且一定是返回一个成功的Promise
- 失败的promise以
{status:'rejected', reason: r}
形式保存到数组,成功的promise以{status:'fulfilled', value: v}
的形式保存到数组。
具体实现
Promise.allSettled = (iterable)=>{const len = Array.isArray(iterable) ? iterable.length : (iterable?.size || 0)const results = Array(len)let cnt = 0return new Promise((resolve, reject)=>{if (!iterable || len === 0) return resolve([])iterable.forEach((p,i)=>{Promise.resolve(p).then((val)=>{results[i] = {status: 'fulfilled', value:val}cnt++if(cnt === len)resolve(results)}).catch(e=>{results[i] = {status:'rejected', reason:e}cnt++if(cnt === len)resolve(results)})})})
}//测试
const p1 = new Promise((resolve, reject)=>{setTimeout(_=>resolve(1), 2000)
})
const p2 = new Promise((resolve, reject)=>{setTimeout(_=>reject('bad'), 1000)
})
Promise.allSettled([p1, p2]).then(results=>{console.log(results);/*[{ status: 'fulfilled', value: 1 },{ status: 'rejected', reason: 'bad' }]*/
})
限制异步并发数
方式一: 先运行limit个promise,然后当其中一个promise结束,唤醒下一个promise执行。
Promise.limitConcurrency = (urls, request, limit)=>{if (!urls || !urls.length) return Promise.resolve([]);return new Promise((resolve)=>{let idx = 0; //资源序号const result = Array(urls.length).fill(null)let finishCnt = 0;function next(){if(idx === urls.length){ //调完return }const curIdx = idx; //记录索引快照,用于then后的回调使用idx++;request(urls[curIdx]).then((val)=>{result[curIdx] = {status:'fulfilled',value: val}console.log({status:'fulfilled',value: val});}).catch(err=>{result[curIdx] = {status:'rejected',reason: err}console.log({status:'rejected',reason: err});}).finally(()=>{next()finishCnt++;if(finishCnt === urls.length){resolve(result)}})}// 先执行limit个,在next内部,当一个promise结束时,再启动下一个。for(let i=0; i<limit && i<urls.length; i++){next();}})
}
测试代码:
// Mock请求
const request = item=> new Promise((resolve, reject)=>{setTimeout(()=>{if(item===2){reject('error')}else{resolve(item)}}, 1000)
})const urls = [1,2,3,4,5] // 请求资源Promise.limitConcurrency(urls, request, 2).then(results=>{console.log(results);
})
你可以按我下面的思路来记住/默写这个方法
1.先搭建框架,传入多个资源、一个执行资源的异步方法和限制并发数limit. 结果返回一个Promise.
Promise.limitConcurrency = (urls, request, limit)=>{if (!urls || !urls.length) return Promise.resolve([]);return new Promise((resolve)=>{const result = Array(urls.length).fill(null); //存放结果})
}
2.先假设有一个next
方法,用来执行一个promise产生结果。一开始要执行limit次next方法。
Promise.limitConcurrency = (urls, request, limit)=>{if (!urls || !urls.length) return Promise.resolve([]);return new Promise((resolve)=>{const result = Array(urls.length).fill(null); //存放结果//next执行promise和处理结果function next(){}//先执行 limit次for(let i=0; i<limit && i<urls.length; i++){next();}})
}
3.完善next方法,当next中的promise结束了,应该唤起下一个next(promise)的执行。并且考虑当所有promise结束,resolve
结果.
Promise.limitConcurrency = (urls, request, limit)=>{if (!urls || !urls.length) return Promise.resolve([]);return new Promise((resolve)=>{const result = Array(urls.length).fill(null); //存放结果let idx = 0;let finishCnt = 0;//next执行promise和处理结果function next(){//记录索引快照,用于then后的回调使用。这一步需要稍微理解下为什么要curIdx。因为request调用后要保存结果到result,不确定当前的异步什么时候结束,而idx在这期间可能是变化了的,所以要保留一个快照。const curIdx = idx; idx++;request(urls[curIdx]).then(()=>{//...}).catch(e=>{}).finally(()=>{next(); //唤醒下一次finishCnt++;if(finishCnt === urls.length){resolve(result)} })}//先执行 limit次for(let i=0; i<limit && i<urls.length; i++){next();}})
}
4.考虑每次promise执行完成结果怎么保存。就是最终版本辣~
//自行默写一遍哦~
方式二: 使用race的特性,只要有一个成功就结束,这样可以做到有一个完成后添加下一个promise执行。
Promise.limitConcurrency = async (urls, request, limit) => {const executing = []; const result = Array(urls.length).fill(null)for (let i=0; i<urls.length; i++) { const p = request(urls[i]).then((val)=>{result[i] = {status:'fulfilled',value: val}console.log({status:'fulfilled',value: val})}).catch(err=>{result[i] = {status:'rejected',reason: err}console.log({status:'fulfilled',reason: err})}).finally(()=>{executing.splice(executing.indexOf(p), 1); //完成后删除,让位给下一个promise}); executing.push(p);if (executing.length >= limit) { //执行队列达到限制,就race。当其中一个promise完成了就会出队让出位置来。await Promise.race(executing); }}if(executing.length)await Promise.all(executing);return result
}
解释:
1.executing 数组存放执行中的promise。
2.遍历url,构建promise,将promise放入executing。同时每个promise在结束后需要被从executing中移除。
3.当executing达到限制,就race并发,待其中一个完成后继续循环(继续往executing加promise)
4.executing中剩下的用all并发,全部完成后就返回结果。
实现串行请求
有一个资源数组(每个资源可以用来创建Promise),实现一个函数,接受这个数组做到串行执行每个资源promise。
具体来说,有[1,2,3]
。像下面这样的就是串行执行了:
// 写法一
createPromise(1).then(()=>{return createPromise(2).then(()=>{})
}).then(()=>{return createPromise(3).then(()=>{})
})
// 写法二
createPromise(1).then(()=>{return createPromise(2).then(()=>{return createPromise(3).then(()=>{})})
})
辅助代码:
const createPromise = (id) => new Promise((solve, reject) =>setTimeout(() => {console.log("promise", id);if(id === 2){reject('2 error')}solve(id);}, 1000)
);// 实现queueExecPromise
// 测试
queueExecPromise([1,2,3]);
实现方式很多。
按写法一的形式来实现: 前一个promise处理了,能在本次迭代拿到前一个promise。前一个promise的then方法中创建本次的promise.
function queueExecPromise(arr){arr.reduce((prePromise, cur)=>{return prePromise.then(val=>{return createPromise(cur)}).catch(e=>{return undefined})}, Promise.resolve())
}
按写法二的形式来实现: 一种递归的实现方式。(其实和并发限制很像,不过limit=1)
function queueExecPromise(arr){function next(){if(arr.length === 0){return Promise.resolve()}const cur = arr.shift()return createPromise(cur).then(val=>{return next()}).catch(e=>{return undefined})}return next()
}
还可以使用async/await 来实现
async function queueExecPromise(arr){for(const item of arr){try {const res = await createPromise(item)console.log(res)} catch (e) {console.log(e)}}
}
实现一个异步调度器
异步调度器 可以添加任务,然后调用flushQueue方法来刷新所有任务(即执行完成所有任务),并且执行任务有并发限制。
class Scheduler {constructor(limit){this.limit = limit;this.queue = [];}add(taskFn){this.queue.push(taskFn)}// 执行flushQueue(){for(let i=0; i<this.limit; i++){this.next()}}next(){if(this.queue.length<=0){return;}const task = this.queue.shift()if(task){task().then(res=>{// console.log(res);this.next()}).catch(e=>{// console.log(e);this.next();})}}
}
测试:
//测试
let scheduler = new Scheduler(2);const addTask = (time, order) => {const task = () => {return new Promise((reslove, reject) => {setTimeout(() => {console.log(order);reslove();}, time);});};scheduler.add(task);
};addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");scheduler.flushQueue();
实现一个具有重试功能的request
实现一个request,可以在失败的时候重试,额外有两个参数:
- interval 重试时间间隔(s)
- retry 重试次数(次)
async function request(url, options, interval=3, retry=5){let leftCount = retry// 闭包+递归实现const _requestData = (_url, _options)=>{return fetch(_url, _options).catch((err)=>{// 失败 => 重试(递归)if(leftCount > 0){return new Promise((resolve, reject)=>{ //=========catch返回的promise将取决于这个promise结果==================setTimeout(()=>{leftCount--;console.log('重试...'+(retry-leftCount));_requestData(_url, _options).then(resolve, reject)}, interval*1000)})}else {throw err}})}return _requestData(url, options)
}
测试:
request('https://www.baidusdf.com',{Method:"POST"}).then(res=>{console.log(res);
}).catch(err=>{console.log('出错了!');console.log(err);
})
/*
重试...1
重试...2
重试...3
重试...4
重试...5
出错了!
TypeError: fetch failed
*/
节流防抖
手撕
节流
简单实现:
// 节流:每单位时间只执行一次。
// 场景:滚动;收藏/点赞按钮
// 实现:基于时间戳,判断时间差是否大于等于delay,来决定是否执行
function throttle(fn, delay){let lastTime = 0return function(){if(Date.now() - lastTime > delay){fn.apply(this, arguments)lastTime = Date.now()}}
}
Ps.也可以使用setTimeout定时器来实现。
function throttle2(fn, delay){let timer = nullreturn function(){if(timer){return;}fn.apply(this, arguments)timer = setTimeout(()=>{timer = null;}, delay)}
}
测试:
let cnt = 0
const throttledFn = throttle((x)=>{console.log(x);
}, 1000)
setInterval(()=>{throttledFn(cnt)cnt++
}, 400)
进阶要求:保证节流的最后一次调用一定执行。
function throttle2(fn, delay){let timer = nulllet lastTimer = null;return function(){if(timer){ // 冻结期if(lastTimer){clearTimeout(lastTimer)}lastTimer = setTimeout(()=>{lastTimer = nullfn.apply(this, arguments)}, delay)return;}fn.apply(this, arguments)timer = setTimeout(()=>{timer = null;clearTimeout(lastTimer)}, delay)}
}
核心思想:
在冻结期间,设置一个setTimeout定时器(对应lastTimer)触发fn调用,这个定时任务和冻结前的setTimeout(对应timer)的定时任务是互相“竞赛”的。
若timer定时先触发则最后一次就被取消,若lastTimer定时先触发就是最后一次执行。
防抖
简单实现:
// 防抖:延迟执行函数,等触发停止了单位时间再执行 (每次触发重新计算延迟时间)
// 场景:搜索输入;调整窗口大小
// 实现: 函数触发后, 延迟单位时间后执行,如果单位时间内触发,则重新计时
function debounce(fn, delay){let timer = nullreturn function(){if(timer){clearTimeout(timer)}timer = setTimeout(()=>{fn.apply(this, arguments)}, delay)}
}
测试:
let cnt = 0
const debouncedFn = debounce((x)=>{console.log(x);
}, 1000, true)
setTimeout(()=>{debouncedFn(cnt)cnt++
}, 500)
setTimeout(()=>{debouncedFn(cnt)cnt++
}, 500)
setTimeout(()=>{debouncedFn(cnt)cnt++
}, 1000)
订阅发布模式
手撕
常见的问法:"写一个EventBus"、"写一个EventMitter"和"写一个订阅发布/观察者模式"。
/* 功能要求:实现on, off, emit, once
*/// 调度中心/中介: 负责订阅事件、通知事件
function Event(){this.listeners = {}
}// 1.注册/订阅
Event.prototype.on = function(eventName, listener){if(this.listeners.hasOwnProperty(eventName)){this.listeners[eventName].push(listener)}else{this.listeners[eventName] = [listener]}
}// 2.注销
Event.prototype.off = function(eventName, listener){if(this.listeners.hasOwnProperty(eventName)){this.listeners[eventName] = this.listeners[eventName].filter(e=>e!==listener)}
}// 3.once 一次注册,用完即注销
Event.prototype.once = function(eventName, listener){const context = thisconst fn = function(...args){context.off(eventName, fn)listener(...args)}this.on(eventName, fn)
}// 4.通知
Event.prototype.emit = function(eventName, ...args){if(this.listeners.hasOwnProperty(eventName)){for(const listener of this.listeners[eventName]){listener(...args)}}
}
写订阅发布模式有两个注意点:
- 需要判断是否有该事件,使用hasOwnProperty;
- once注册,不是将listener直接注册,而是注册一个包装函数,用完即注销(这种方式比较优雅)。
另外,有些面试官会提到,可不可以订阅后产生一个ID,后续通过ID取消某个订阅?
解决这个问题,只需要Event维护一个全局的id,然后原来的listeners[eventName]
从数组结构,改为对象结构({[id]:callback}
, id做key,回调函数做值)。
正则相关
正则基础知识
字符串的方法
1.match方法
string.match(regexp)
匹配到项则返回一个数组,没有则返回null
。数组的内容则根据正则是否是g
模式有区分。
- 不是g模式:
[0]
匹配的完整文本,后续元素表示捕获组(括号()
匹配的文本),最后两项分别是index(匹配的字符串的起始位置)、input(原字符串) - 是g模式:返回所有匹配的字符串构成的数组(此时没有捕获组、index和input)
2.replace方法
string.replace(regexp, fn)
,当regexp不是g模式,则只调用一次fn来替换。当regexp是g模式则全局匹配了多少次就会调用多少次fn来替换。举个例子如下:
//第二个回调函数fn的参数分别是 匹配的字符串、捕获组、匹配字符串的起始索引和原始字符串引用(和match方法的非g模式是一样的)
export function test(){const res = "1abc".replace(/(ab)(c)/g, (match, g1, g2, index, intput)=>{console.log(match) //abcconsole.log(g1) //abconsole.log(g2) //cconsole.log(index) //1console.log(intput) //1abcreturn '0'})console.log(res); //10
}
正则对象的方法
1.test方法
regexp.test(string)
。测试字符串是否匹配正则,返回Boolean。
2.exec方法
regexp.exec(string)
。g模式下,返回结果和match的非g模式几乎一样。不同的是正则对象可以多次调用exec方法,从而不断的匹配下一项(符合全局模式行为),举个例子:
export function test(){const reg = /(ab)(c)/gconst str = "1abc2abc"const res = reg.exec(str)const res2 = reg.exec(str)console.log(res); // [ 'abc', 'ab', 'c', index: 1, input: '1abc2abc', groups: undefined ]console.log(res2); //[ 'abc', 'ab', 'c', index: 5, input: '1abc2abc', groups: undefined ]
}
手撕
千分位
function formatPrice(price) {return String(price).replace(/\B(?=(\d{3})+$)/g, ',');
}//测试
console.log(formatPrice('888999')); //888,999
console.log(formatPrice('7888999')); //7,888,999
console.log(formatPrice('77888999')); //77,888,999
解释:
- 首先
(\d{3})+$
匹配3个数字3个数字一组的,并以3个数字结尾。 ?=
表示后面的正则内容仅仅是断言(不是实际匹配),换句话说就是仅断言是否能匹配,但不作为最终匹配结果,这样就不会被replace掉。\B
表示非单词边界(即前面应该有字符)
插值语法 {{}}匹配
如题:
/*
将插值{{}}的内容替换
*/
let str = "我是{{name }},年龄{ {age }},爱好{{ hobby}}";
const obj = {name:'疯狂踩坑人',age: 24,hobby: 'buy'
}
代码:
function replaceVar(str){return str.replace(/\{\s*?\{(.*?)\}\s*?\}/g, (matchStr, g1)=>{return obj[g1.trim()]})
}
console.log(replaceVar(str)); //我是疯狂踩坑人,年龄24,爱好buy
url的params获取
function getUrlQuery(url, key) {const queryObj = {}const matches = /.+?\?(.+)/.exec(url)if(matches){//matches是数组,从第二个参数开始是捕获分组if(!matches[1])return undefinedconst hashStartIndex = matches[1].indexOf('#')const query = hashStartIndex===-1?matches[1]: matches[1].slice(0,hashStartIndex) //去hashif(query){const queries = query.split('&') queries.reduce((acc, item)=> {const kw = item.split('=')const key = kw[0].trim()if(acc[key] === undefined){acc[key] = kw[1]}else{acc[key] = Array.isArray(acc[key]) ? [...acc[key], kw[1]] : [acc[key], kw[1]]}return acc}, queryObj)}}return key ? queryObj[key] : queryObj
}const query = getUrlQuery('http://www.google.com/search?q=javascript&aqs=chrome.0.0l6j69i60j69i61j69i60.3518j1j7&sourceid=chrome&test=1&test=2#title');
console.log(query);
一些API实现
JSON.stringify (wxg经典的一道手撕)
只考虑普通字面对象,如何实现JSON.stringify
呢?先了解下一些规则
- 对于对象、数组会递归序列化,使用
{}
、[]
表示对象和数组边界 - 会丢失:Undefined/Function/Symbol
- 字符串需要
""
边界,其他基础数据类型Number/Boolean/Null 等转字符串
举例:
const obj = {name: '疯狂踩坑人',children: ['good', 'bad', {rank: 1, has: false}],say: ()=>{console.log('hhh')},x:undefined,y:null,z:Symbol(1),1:1,
}// 转换后
// {"1":1,"name":"疯狂踩坑人","children":["good","bad",{"rank":1,"has":false}],"y":null}
实现:
JSON.mystringify = (obj)=>{const type = typeof objif(type === 'object' && obj!==null){ //对象if(Array.isArray(obj)){let ans = ''for(const item of obj){const value = JSON.mystringify(item)if(value){if(ans!==''){ans += ','}ans += value}}return `[${ans}]`}else{ //简单处理, 其实还要考虑Map, Set let ans = ''for(const key of Object.keys(obj)){const value = JSON.mystringify(obj[key])if(value){if(ans!==''){ans += ','}ans += `"${key}":${value}` }}return `{${ans}}`}}else if( /undefined|function|symbol/.test(type)){// 忽略function 、undefined 、symbolreturn }else{// 基础类型return type === 'string' ? `"${obj}"` : String(obj)}
}
面试官可能会深挖的点:
1.考虑Map,Set,情况是怎么样的?
Map和Set对象会被处理为空对象{}
。
2.循环依赖了,怎么处理?
使用一个WeakSet
来记录已访问过的对象,如果遇到了访问过的,说明循环依赖了,抛出错误
parseInt
leetcode有一道类似的题 字符串转换整数 (atoi)
这里实现一个简单版本(一个正整数字符串 转 数字类型,忽略第二个参数,就按10进制来).
比如:"42" -> 42
代码:
function parseInt(str){const baseCode = "0".charCodeAt(0)let ans = 0;for(let i=0; i<str.length; i++){const curCode = str.charCodeAt(i)const num = curCode - baseCodeans *= 10;ans += num;}return ans;
}// 测试
console.log(parseInt("123")); //123
console.log(parseInt("042")); //42
这里是有一点技巧的:
- 利用ASCII码做减法,算出字符的数值
- 从左到有遍历,每次对ans先乘10,一方面完成权重增加,另一方面有效处理了前导0。
trim
这是字符串的trim
方法,作用:删除两端的空格。
function trim(str){let i=0, j=str.length-1;while(i<=j && str[i]===' '){i++;}while(i<=j && str[j]===' '){j--;}return str.substring(i, j+1)
}// 测试
console.log(trim("x ab c d 1 ") + '_end');
console.log(trim(" x ab c d 1") + '_end');
console.log(trim(" x ab c d 1 ") + '_end');
lodash.get
function get(obj, str, defaultVal=undefined){const strType = typeof strlet path = []if(strType === 'string'){path = str.split('.')}else if(Array.isArray(str)){path = str}else{return defaultVal}let i=0; let parent = objwhile(i<path.length ){const type = typeof parent[path[i]]if(type === 'object' && type !== null){ //继续搜parent = parent[path[i]]i++;}else{return parent[path[i]]===undefined ? defaultVal: parent[path[i]]}}return parent;
}
测试:
const _ = {get
}const object = { a: { b: { c: 3 } }
};console.log(_.get(object, 'a.b.c')); // 输出: 3
console.log(_.get(object, 'a.b.x', 'default')); // 输出: 'default'console.log(_.get(object, ['a', 'b'])); // { c: 3 }
console.log(_.get(object, ['a', 'b', 'c'])); // 输出: 3
递归/迭代 思想
1.路径命名转树(字节面)
// 将arr转为output的形式
const arr = ['A', 'A.B', 'A.B.C', 'A.C', 'A.C.D', 'E', 'E.F', 'E.G', 'E.G.H']
const output = [{name: 'A',children: [{name: 'B',children: [{name: 'C',children: []}]},{name: 'C',children: [{name: 'D',children: []}]}]},{name: 'E',children: [{name: 'F',children: []},{name: 'G',children: [{name: 'H',children: []}]}]}
]
实现思路:路径字符串转数组,然后沿数组一级级找(没找到就创建节点挂载到主体上,找到节点则作为新主主体)。
function transform(arr){const root = {children:[]}; //顶级节点// 根据path一级级往下找,插入到root中const _insert = (path)=>{let cur = root; for(const item of path){const newCur = cur.children.find(child=>child.name === item);if(newCur){ //找到节点,直接更新主体cur = newCur}else{ //没找到节点,创建节点,挂载到主体上const s = {name: item, children:[]}cur.children.push(s)cur = s;}}}for(const item of arr){//1.转数组const names = item.split('.')//2.沿数组找,插入_insert(names)}return root.children;
}
2.解析DOM属性(美团面)
<div><span><a>网址1</a></span><span><a>网址2</a><a>网址3</a></span>
</div>
已知一个DOM结构如上,转换为下面的JSON格式
{tag: 'DIV',children: [{tag: 'SPAN',children: [{ tag: 'A', children: [] }]},{tag: 'SPAN',children: [{ tag: 'A', children: [] },{ tag: 'A', children: [] }]}]
}
实现代码:
function dom2json(domTree){const json = {}if(typeof domTree === 'object' && domTree !== null){json.tag = domTree.tagNamejson.children = domTree.childNodes.map(child => dom2json(child))}return json;
}
3.对象的key驼峰化命名(腾讯面)
// 输入
const input = {err_msg:'hhh',my_real_data: {list: ['item1', {list_children: []}]},count: 13,errors: [{field:'name', error_msg:'xx'}]
}// 输出
{errMsg: 'hhh',myRealData: { list: [ 'item1', listChildren:[] ] },count: 13,errors: [ { field: 'name', errorMsg: 'xx' } ]
}
实现代码:
function transformKey(key){return key.replace(/_([a-z])/g, (matchStr, g1)=>{return g1.toUpperCase();})
}function transformObj(obj){const type = typeof obj;// 对象if(type === 'object' && type!==null){if(Array.isArray(obj)){return obj.map(item=>transformObj(item))}else{let newObj = {}for(const key of Object.keys(obj)){const newKey = transformKey(key)// console.log(newKey);newObj[newKey] = transformObj(obj[key])}return newObj}}else{// 基础数据类型return obj;}}
4.扁平数组转嵌套数组/Tree(猿辅导面)
//输入
const data = [{id:1, name:'a', pid: 0},{id:2, name:'bb', pid: 6},{id:3, name:'cc', pid: 5},{id:4, name:'dd', pid: 3},{id:5, name:'ee', pid: 6},{id:6, name:'ff', pid: 0},
]
//转换结果
{"id": 0,"children": [{"id": 1,"name": "a","pid": 0},{"id": 6,"name": "ff","pid": 0,"children": [{"id": 2,"name": "bb","pid": 6},{"id": 5,"name": "ee","pid": 6,"children": [{"id": 3,"name": "cc","pid": 5,"children": [{"id": 4,"name": "dd","pid": 3}]}]}]}]
}
实现代码:
function buildTree(arr){// [pid]:nodes , pid为key记录节点const map = new Map();arr.forEach(item=>{if(map.has(item.pid)){map.set(item.pid, map.get(item.pid).concat(item))}else{map.set(item.pid, [item])}})// root = {id:0, children:[]}// 含义:找到每个节点的childrenfunction _dfs(nodes){ for(const node of nodes){if(map.has(node.id)){node.children = map.get(node.id);_dfs(node.children)}}}const root = {id:0}_dfs([root])return root
}
排序算法
手撕
快速排序
可以去试试这道leetcode 排序数组
function partition(nums: number[], i:number, j:number){const idx = Math.floor(Math.random()*(j+1-i)) + i;[nums[i], nums[idx]] = [nums[idx], nums[i]];const x = nums[i];while(i<j){while(i<j && nums[j]>=x){j--;}nums[i] = nums[j]while(i<j && nums[i]<=x){i++}nums[j] = nums[i]}nums[i] = x;return i;
}function quickSort(nums: number[], i:number, j:number) {if(i<j){const mid = partition(nums, i, j)quickSort(nums, i, mid-1)quickSort(nums, mid+1, j)}
}function sortArray(nums: number[]): number[] {quickSort(nums, 0, nums.length-1)return nums;
};
快排应该是比较经常被问到的了,建议10min内能写完代码。并且能分析时间复杂度。