前端面试高频手写代码题
一、实现一个解析URL参数的方法
方法一:String和Array的相关API
第一种方法主要是利用字符串分割和数组操作,拿到关键的字符串,再做一下类型转换,组成最终的结果。主要技术点是要熟悉String和Array的相关API,灵活运用。
const getUrlParams = (url) => {
const arrURL = url.split('?').pop().split('#').shift().split('&');
console.log(arrURL)
let obj = {};
arrURL.forEach(item => {
const [k,v] = item.split('=');
obj[k] = v;
})
return obj;
};
const url = 'http://sample.com/?a=1&b=2&c=xx&d=2#hash';
console.log(getUrlParams(url))
方法二: Web API 提供的 URL
第二种方法利用了 Web API 提供的 URL 和 URLSearchParams 对象实现的,用起来非常简单,缺点是兼容性不是很完美,不兼容IE系列浏览器。
const getUrlParams2 = (url) => {
const u = new URL(url)
const s = u.searchParams
let obj = {};
// 这里的
s.forEach((v,k) => {
obj[k] = v;
})
console.log(obj)
return obj;
};
const url = 'http://sample.com/?a=1&b=2&c=xx&d=2#hash';
console.log(getUrlParams2(url))
方法三:正则表达式+string.replace方法
第三种方法通过正则表达式(捕获组,匹配不在集合中的字符)解析标准的query-string键值对字符串,然后巧妙地利用 string.replace方法的第二个参数可以作为回调函数进行一些操作,得到最终结果,写起来是最简洁的,但是写正则真的是头疼。
正则表达式 /([?&=]+)=([&]+)/g 匹配 URL 中的查询参数。其中 [^?&=]+ 匹配参数名,= 匹配参数名和参数值之间的等号,[^&]+ 匹配参数值。
调用 replace 方法将匹配到的字符串替换为箭头函数的返回值。这里的第一个参数 _ 表示匹配到的整个字符串,不需要使用,用下划线表示忽略该参数。第二个参数 k 表示匹配到的参数名,第三个参数 v 表示匹配到的参数值。
const getUrlParams3 = (url) => {
// 定义一个 parse url.search 的方法
url = url.split('#').shift();
const obj = {};
url.replace(/([^?&=]+)=([^&]+)/g, (_, k, v) => (obj[k] = v));
return obj;
};
const url = 'http://sample.com/?a=1&b=2&c=xx&d=2#hash';
console.log(getUrlParams3(url))
二、Call 、Apply、 Bind
call:
改变 this 指向用的,可以接收多个参数
delete: fn如果不及时删除,可能会导致内存泄漏,因此需要使用 delete 关键字将其从上下文对象中删除。
唯一的 Symbol 值 fn,并将其作为属性添加到 ctx 对象中,这样就避免了属性名的冲突。
Function.prototype.mycall = function(ctx, ...args){
ctx = ctx || globalThis;
let fn = Symbol();
ctx[fn] = this;//this是函数的调用者,这里是foo
let result = ctx[fn](...args);
delete ctx[fn];//释放内存
return result
}
let obj = {
name:"张三"
}
function foo(){
return this.name;
}
// 就是把 foo 函数里的 this 指向,指向 obj
console.log(foo.mycall(obj))
apply:
原理同上,只不过 apply 接收第二个参数是数组,不支持第三个参数
但这里有一些点可以进一步改进和考虑:
全局对象的使用:在非浏览器环境(例如 Node.js)中,window 对象是不可用的。你可以使用 globalThis 来代替,因为它是一个在所有环境中都可用的全局对象引用。
参数检查:你在代码中直接使用了 arguments[1],这样做当确实有第二个参数时没问题,但建议还是加上数组检查,以避免意外地传入非数组参数。
使用 arguments 对象来判断是否传递了第二个参数,第一个参数是ctx,如果传递了,将其作为参数列表使用展开运算符 … 解构到一个数组中
Function.prototype.myapply = function(ctx, args) {
ctx = ctx || globalThis; // 使用globalThis来确保在所有环境中都正确
let fn = Symbol();
ctx[fn] = this;
let result;
if (Array.isArray(args)) {
result = ctx[fn](...args);
} else {
result = ctx[fn]();
}
delete ctx[fn]; // 清除临时添加到ctx的方法
return result;
}
let obj = {
name: "张三"
}
function foo() {
return this.name;
}
console.log(foo.myapply(obj, [])); // 张三
bind
bind 不会立即执行,会返回一个函数
1、函数可以直接执行并且传参,如 foo.myBind(obj, 1)(2, 3),所以需要 [ …args, …arguments ]合并参数
2、函数也可以 new,所以要判断原型 this instanceof fn
Function.prototype.myBind = function (ctx, ...args) {
const self = this
const fn = function(){}
const bind = function(){
const _this = this instanceof fn ? this : ctx
return self.apply(_this, [...args, ...arguments])
}
fn.prototype = this.prototype
bind.prototype = new fn()
return bind
}
call、apply、bind的区别
1、都可以改变 this 指向
2、call 和 apply 会立即执行,bind 不会,而是返回一个函数
3、call 和 bind 可以接收多个参数,apply 只能接受两个,第二个是数组
bind 参数可以分多次传入
三、instanceof
接受两个参数,判断第二个参数是不是在第一个参数的原型链上
function myInstanceof(left, right) {
// 获得实例对象的原型 也就是 left.__proto__
let left1 = Object.getPrototypeOf(left)
// 获得构造函数的原型
let prototype = right.prototype
// 判断构造函数的原型 是不是 在实例的原型链上
while (true) {
// 原型链一层层向上找,都没找到 最终会为 null
if (left1 === null) return false
if (prototype === left1) return true
// 没找到就把上一层拿过来,继续循环,再向上一层找
left1 = Object.getPrototypeOf(left1)
}
}
let arr = [1,2,3]
let str = '123'
// 第一个参数是实例对象,第二个参数是构造函数
console.log(myInstanceof(str,Array));
四、数组去重
方法一、new Set()
function unique(arr){
return Array.from(new Set(arr))
}
let arr = [1,1,2,2,3,3,4,4]
console.log(unique(arr))
方法二、Map 或 对象
用空对象 let obj ={}利用对象属性不能重复的特性
map方法:
function unique(arr){
let map = new Map() // 或者用空对象 let obj ={}利用对象属性不能重复的特性
let ar = []
arr.forEach((item) => {
if(!map.has(item)){// 如果是对象的话就判断 !obj[item]
map.set(item,true) // 如果是对象的话就 obj[item] = true 其他一样
ar.push(item)
}
})
return ar;
}
let arr = [1,1,2,2,3,3,4,4]
console.log(unique(arr))
对象方法:
function unique(arr){
let obj = {}
let ar = []
arr.forEach((item) => {
if(!obj[item]){// 如果是对象的话就判断 !obj[item]
obj[item] = true // 如果是对象的话就 obj[item] = true 其他一样
ar.push(item)
}
})
return ar;
}
let arr = [1,1,2,2,3,3,4,4]
console.log(unique(arr))
方法三、indexOf 或者 includes
includes方法:
function unique(arr){
let ar = [];
arr.forEach((item) => {
if(!ar.includes(item)){
ar.push(item)
}
})
return ar;
}
let arr = [1,1,2,2,3,3,4,4]
console.log(unique(arr))
indexOf方法:
function unique(arr){
let ar = [];
arr.forEach((item) => {
if(ar.indexOf(item) == -1){
ar.push(item)
}
})
return ar;
}
let arr = [1,1,2,2,3,3,4,4]
console.log(unique(arr))
方法四、filter + indexOf
function unique(arr){
let ar = []
ar = arr.filter((item,index) => {
return arr.indexOf(item) == index
})
return ar;
}
let arr = [1,1,2,2,3,3,4,4]
console.log(unique(arr))
扩展:map set 常用方法和 object的常用方法
1.Set数据类型:
集合 :里面的元素不能重复
Set是 一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。
常用的方法:
add : 添加一个set元素
delete : 删除一个set元素
has : 查看元素是否存在
size属性 : 查看set中的元素数量
clear : 清空set集合
Symbol.iterator :迭代数据
foreach:循环遍历数据
2.Map数据类型
map与Object非常类似。map与Object的最大区别是:map的key可以是任何数据类型,object的可以只能是字符串,JavaScript支持的所有类型都可以当作Map的key
常用的方法:
set : 添加一个map元素 内置两个参数 第一个是键 第二个是值
delete : 删除一个map元素
has : 查看元素是否存在
size属性 : 查看map中的元素数量
get : 获取一个map集合元素
clear : 清空map集合
foreach:循环遍历数据
3.map 与 set 的关系
1.Map是键值对,Set是值的集合,当然键和值可以是任何的值;
2.Map可以通过get方法获取值,而set不能因为它只有值;
3.都能通过迭代器进行for…of或者foreach遍历;
4.Set的值是唯一的可以做数组去重,Map由于没有格式限制,可以做数据存储
4.ES6中 Object 的常用方法:
Object.is() 方法判断两个值是否是相同的值
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。
Object.getOwnPropertyDescriptors() 回指定对象所有自身属性(非继承属性)的描述对象。
Object.setPrototypeOf() 为现有对象设置原型,返回一个新对象
Object.getPrototypeOf() 方法返回指定对象的原型
Object.keys() 返回所有可遍历( enumerable )属性的键名。
Object.values(),返回所有的可遍历属性的内容。
Object.entries() 方法返回一个给定对象自身可枚举属性的键值对数组。
五、数组扁平化:
就是把多维数组变成一维数组
let arr = [1,4,[1,[2,[3]]]]
let brr = arr.flat(Infinity);
console.log((brr))
六、防抖与节流:
防抖:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>debounce</title>
</head>
<body>
<p>debounce</p>
搜索 <input id="input1">
<script>
// 默认延迟200毫秒,如果传入的有值,就优先传入的值
function debounce(fn, delay = 200) {
let timer = null;
return function () {
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn.apply(this, arguments)//透传this和参数
timer = null;
},delay)
}
}
const input1 = document.getElementById('input1')
input1.addEventListener('keyup', debounce(()=>{
console.log("搜索", input1.value)
}),300)
</script>
</body>
</html>
节流:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>throttle</title>
</head>
<body>
<p>throttle</p>
<div id="div1" draggable="true" style="width: 100px; height: 50px; background-color: #ccc; padding: 10px;">可拖拽</div>
<script>
// 默认延迟200毫秒,如果传入的有值,就优先传入的值
function throttle(fn, delay = 200) {
let timer = null;
return function () {
if(timer) return
timer = setTimeout(()=>{
fn.apply(this, arguments)//透传this和参数
timer = null;
},delay)
}
}
const div1 = document.getElementById('div1')
div1.addEventListener('drag', throttle((e)=>{
console.log("鼠标的位置", e.offsetX, e.offsetY)
}),300)
</script>
</body>
</html>
七、new
function myNew(fn,...args){
// 不是函数不能 new
if(typeof fn !== "function"){
throw new Error('TypeError')
}
// 创建一个继承 fn 原型的对象
const newObj = Object.create(fn.prototype);
// 将 fn 的 this 绑定给新对象,并继承其属性,然后获取返回结果
const result = fn.apply(newObj, args);
// 根据 result 对象的类型决定返回结果
return result && (typeof result === "object" || typeof result == "function") ? result : newObj;
}
八、create
function mycreate(obj) {
function fn(){}
fn.prototype = obj;
return new fn()
}
补充:对象的三种创建方法:
1、字面量方法: let obj = {} 、Object.create方法、new方法
2、三者的区别:
- 字面量和new关键字创建的对象是Object的实例,原型指向Object.prototype,继承内置对象Object
- Object.create(arg, pro)创建的对象的原型取决于arg,arg为null,新对象是空对象,没有原型,不继承任何对象;arg为指定对象,新对象的原型指向指定对象,继承指定对象
九、3种继承:
1、 function + call
function parent() {
this.name = '张三';
}
function child(){
this.age = 19;
parent.call(this);
}
let obj1 = new child()
console.log(obj1)
2、class + extends
一定要记得写super()
注意:使用了class的是ES6继承,function的原型继承和call继承都是ES5继承
class parent{
constructor(){
this.name = '张三'
}
}
class child extends parent{
constructor(){
super()
this.age = 19
}
}
let obj = new child();
console.log(obj)
3、原型
在子类的原型上new一个父类
如果光打印子类可能看不出来继承了,要特地去打印子类没有但是父类有的属性,会发现子类也有,那么就是实现继承了
function parent() {
this.name = '张三';
}
function child(){
this.age = 19;
}
child.prototype = new parent()
let obj1 = new child()
console.log(obj1,obj1.name)
十、深拷贝与浅拷贝
1、加了{} , 使用Object.assign({},obj)层次大于2的都是浅拷贝,第一层都是深拷贝
2、如果不加{} — Object.assign(obj) — 全都浅拷贝
let obj = {name:'张三', age:19,work:['it']}
let obj1 = Object.assign({},obj);
// let obj1 = Object.assign(obj)
// 浅拷贝 -- 使用Object.assign({},obj); 层次大于2的都是浅拷贝
obj1.work[0] = 'web'
console.log(obj)
console.log(obj1)
// 深拷贝--Object.assign({},obj);第一层都是深拷贝
obj1.name = 'lisi'
console.log(obj)
console.log(obj1)
// 如果不加{} --- Object.assign(obj) --- 全都浅拷贝
疑问:
1.为什么这里可以用通过obj[k] = v;设置值?我记得对象的数组访问的k必须得是字符串
你提到了一个非常重要的点。在JavaScript中,对象的键确实是字符串。但JavaScript为我们提供了便利性,当我们尝试使用非字符串作为对象键时,它会在内部将其转换为字符串。
在你的代码中,k
很可能已经是一个字符串,因为它是通过 split
方法从URL字符串中提取出来的。但即使它不是,当你执行 obj[k] = v;
时,JavaScript会自动将 k
转换为字符串。
下面是一个简单的示例来说明这个行为:
let obj = {};
let numberKey = 123;
obj[numberKey] = "value";
console.log(obj); // 输出:{ "123": "value" }
let boolKey = true;
obj[boolKey] = "valueForTrue";
console.log(obj); // 输出:{ "123": "value", "true": "valueForTrue" }
从上面的例子可以看出,虽然我们使用了数字和布尔值作为键,但在对象中它们都被转换为了字符串。
因此,当你在 getUrlParams
函数中执行 obj[k] = v;
时,你确实是在使用字符串作为对象的键,即使JavaScript需要在内部进行类型转换。
2.最后console.log(foo.mycall(obj))只给foo传递了一个参数是obj,所以这个obj是ctx还是…args?
在这个例子中:
console.log(foo.mycall(obj));
obj
是传递给 mycall
方法的第一个参数,因此 obj
是 ctx
。
在 mycall
的定义中:
Function.prototype.mycall = function(ctx, ...args){
...
}
ctx
是mycall
方法的第一个参数。...args
会捕获所有其它传递给mycall
的参数(从第二个参数开始)。
在给定的例子中,我们只传递了一个参数给 mycall
,所以 ctx
是 obj
,而 ...args
是一个空数组,因为我们没有传递任何额外的参数。如果我们传递了更多的参数,它们就会被捕获到 ...args
中。
例如:
function foo(a, b) {
return this.name + a + b;
}
console.log(foo.mycall(obj, 'A', 'B')); // 这里 'A' 和 'B' 会被捕获到 ...args
在这个情况下,ctx
仍然是 obj
,但 ...args
现在是 ['A', 'B']
。
3.let fn = Symbol();是什么意思?
在JavaScript中,Symbol
是一种基本数据类型(与 number
、string
、boolean
等同级)。它的主要用途是创建一个独一无二的标识符。当使用 Symbol
时,每次调用都会返回一个全新的、唯一的值。
在你提供的代码中:
let fn = Symbol();
这行代码创建了一个新的唯一标识符,并将其赋值给变量 fn
。
Symbol
的主要用途之一是作为对象的属性键,确保属性不会与对象上的其他属性冲突。这是因为,两个 Symbol
,即使是通过相同的描述创建的,也永远不会相等。
const symbol1 = Symbol('description');
const symbol2 = Symbol('description');
console.log(symbol1 === symbol2); // false
在你提供的代码的上下文中,使用 Symbol
作为对象属性的键是为了避免潜在的属性名冲突。因为 Symbol()
每次都会创建一个独特的标识符,所以不会与 ctx
上的任何现有属性冲突。
例如,这段代码中:
ctx[fn] = this;
这里,fn
(即我们之前创建的 Symbol
)用作 ctx
对象的属性名。由于 fn
是一个唯一的 Symbol
,我们可以确保我们不会无意中覆盖 ctx
中的任何现有属性。
4.那么打印symbol会显示什么?为什么它是唯一的,它是怎么生成唯一的标识符的?
- 打印
Symbol
显示什么:
当你打印一个 Symbol
值时,你会看到形如 Symbol(description)
的输出,其中 “description” 是你在创建 Symbol
时可选地提供的描述字符串。
const mySymbol = Symbol('myDescription');
console.log(mySymbol); // 输出:Symbol(myDescription)
注意:这个描述字符串并不影响 Symbol
的唯一性。两个有相同描述的 Symbol
仍然是不同的。
- 为什么它是唯一的:
Symbol
的唯一性是其设计的核心特点。JavaScript 内部确保每次调用 Symbol()
都会返回一个全新的、唯一的值。这样做可以确保你不会无意中覆盖对象的其他属性,从而使得 Symbol
成为创建私有属性或避免属性名冲突的理想选择。
- 它是怎么生成唯一的标识符的:
JavaScript 的具体实现是未公开的,但生成唯一值的方法很多,例如使用时间戳、随机数、内部计数器等,或它们的组合。主要的点是,JavaScript 运行时确保了每次调用 Symbol()
都会产生一个独一无二的值,无论其背后的具体机制是什么。
要了解更多关于如何生成唯一标识符的细节,你可能需要查看某个具体的JavaScript引擎(如 V8 或 SpiderMonkey)的源代码或相关文档。但一般来说,对于日常的开发工作,知道每个 Symbol
都是唯一的,并且不等于任何其他的 Symbol
,就足够了。