前端面试题合集

499 阅读15分钟

2023年注定是灰暗的一年,以为度过了2022就会迎来曙光,然而有些事情就是这样不如意,不过我相信情况会越来越好,疫情后的5年,我们拭目以待。年前和年后经历了一些面试,整理了些面试题,可以让自己经常回头温习下。如果哪里有问题,希望你不吝赐教,感谢。如果对你有帮助,希望点赞收藏不迷路。

1.简单说下WebSocket

为什么会创造出websocket ?

没有websocket之前,客户端和服务端通信,只能从客户端向服务器发起请求拿到数据。如果数据的实效性比较高,那么通常的做法是客户端开启轮巡,每隔一段时间就发送一次请求,这样浪费性能和带宽。websocket的出现就是解决这个问题,websocket的连接客户端和服务端都可以主动发送数据。

Websocket的特点 ?

  • 建立在TCP协议之上,服务端的实现比较容易
  • 与http协议有着良好的兼容性,默认端口也是80和443;并且握手阶段也是http协议,因此握手时不容易被屏蔽,能通过各种Http代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议表示符是ws,如果加密则是wss
  • 只有一次握手

Websocket请求头

  • Get ws://localhost:3000/xxx Http 1.1
  • Host: localhost
  • Connection: Upgrade
  • Upgrade: Websocket
  • Origin: http://localhost:3000
  • Sec-WebSocket-Key: xxx
  • Sec-WebSocket-Version:xxx

Websocket 响应头

  • Http1.1 101 Switching Protocals //代表切换协议成功
  • Upgrade: WebSocket
  • Connection: Upgrade
  • Sec-WebSocket-Accept: xxx

Http2.0 可以服务端推送,那Http2.0和WebSocket有什么区别呢?

主要区别在于是否主动推送,比如A与B聊天,A将信息发送到WebSocket服务器,服务器接到信息后主动推送到B。而Http2.0的服务端推送,一般是指比如请求服务器的index.html资源,这时服务器会主动将与index.html有关的js、css、图片等资源推送给客户端,防止多次请求。并切Http2.0必须是客户端先发请求,然后我才知道要推送什么。所以就IM来说,Http2.0服务端推送和WebSocket还是有区别的。

2.介绍下Set数据结构

特点:

  • Set集合通过构造函数创建
  • 任何具有iterable接口的对象都可以作为Set构造函数的参数
  • 存入的值不能重复,存取有序
  • 内部判断元素是否相等算法类似===,区别是set内的算法认为两个NaN相等,而===认为不等

常用方法:

  • add(value) 添加元素,返回Set结构本身
  • delete(value) 删除元素,返回boolean类型结果,表示是否删除成功
  • has(value) 返回boolean类型结果,表示set是否包含此元素
  • clear() 清除所有成员,没有返回值
  • keys( ) / values( ) 分别返回key和value的遍历器 如:SetIterator{2,4,5,8}, keys和* values是一样的
  • entries( ) 返回键值对的遍历器,如 SetIterator {2 => 2, 5 => 5, 6 => 6, 8 => 8}
  • forEach( (value,key,set)=>{ } ) 遍历集合

WeakSet

weakset与set类似,存储的值也都不能重复,但是它与set有两点区别:

  • weakset只能存储引用类型的对象,不能存储基本类型,否则会报错。
  • weakset中的对象都是弱引用的,即垃圾回收机制不考虑weakset对该对象的引用,也就是说,如果其他对象不再引用该对象,那么垃圾回收机制就会回收该对象占用的内存,不考虑该对象是否还存在于weakset中。
  • 由于垃圾回收机制运行会导致weakset内部成员个数变动,所以ES6规定weakset不可以遍历。

3.介绍下Map数据结构

Map 特点:

* 通过构造函数 new Map( )创建,任何类似这种形式的数据都可以作为参数[['name', '张三'] , [ 'title', 'author' ] ]
* map键值重复,value值会覆盖;判断键的相等性类似===,只不过map中认为两个NaN相等

方法:

size( )
set( key, value ) 任何值都可以作为key值
get( key )
has( key )
delete( key )
clear( )
keys( )
values( )
entries( )
forEach( )

WeakMap :

  • weakmap只接受对象作为键值,null不可以作为键值
  • weakmap键名所指向的对象不计入垃圾回收机制

为什么需要WeakMap这样的数据结构呢?

const e1 = document.getElementById("e1");
const e2 = document.getElementById("e2");
const arr = [
  [e1, '元素e1'],
  [e2, '元素e2']
];

//不需要arr后必须手动删除引用
arr[0] = null;
arr[1] = null;

如上结构中,我们在arr数组中保存了两个Dom元素,这就形成了arr对e1和e2的引用;一旦不在需要arr我们必须手动删除内部的引用,否则就会造成内存泄漏。

为了解决这种情况,WeakMap应运而生,保存在其键内的对象,不会被强引用;如果没有其他对象对其引用,那么键对象随时可能会被垃圾回收器回收。

注意:WeakMap的weak是指对键对象的弱引用,而不是值;值是强引用的。

4. Object.is、==、=== 区别

  1. SameValue标准,接口实现是Object.is( ),满足以下条件,则判断为相等:
  • 都是undefined
  • 都是null
  • 都是true或false
  • 都是相同长度、相同字符、按相同顺序排列的字符串
  • 都是相同对象,指同一个对象的引用
  • 都是数字,都是+0,都是-0,都是NaN,都是同一个值非零且不是NaN
  1. SameValueZero标准,和SameValue的区别在于+0和-0是相等的,Array.prototype.includes( )就是用这种比较
  2. === 数组indexOf就是使用这种比较,和Object.is基本相似,但有2点区别:
  • 一是 === 中 +0 和 -0 相等,而Object.is中不相等
  • 二是 === 中NaN不相等,而Object.is中是相等的
  1. == 会尝试进行类型转换,== 中null只和自身和undefined是互等的。
1 == '1' //true
false == 0 //true
false == "" //true

5.CSS加载会阻塞Dom的解析和渲染吗?

Css加载会阻塞Dom解析吗?

正常情况下不会阻塞Dom的解析,Css加载解析和Dom的加载解析是并行关系,所以不会阻塞;但是Css会阻塞Js执行,所以如果Js后面有Dom标签,会阻塞其后面的Dom的渲染的;

Css加载会阻塞Dom渲染吗

会阻塞Dom的渲染,因为Dom Tree + CSSOM ==> Render Tree , 也就是说Render Tree依赖Dom和Css,所以必须Css加载完才会渲染。但这也不是绝对的,因为现代浏览器为了减少白屏时间让用户更快的看到页面,会有边解析边渲染的机制,也就是说解析和渲染是并行的,不是串行的;比如在body中某个标签用link引入外部样式,link前的dom是不会受影响的,只有link后面的dom会被阻塞。

Css加载会阻塞Js执行吗?

会的

网络相关

1. 强缓存和协商缓存

强缓存

缓存规则:
* 浏览器缓存中不存在缓存结果和标识,缓存失效,直接向服务器发起请求
* 浏览器缓存中存在缓存,缓存已经失效,走协商缓存
* 浏览器缓存中存在缓存,并且未失效,直接返回结果

缓存标识:Expires (http1.0) 和 Cache-Control (http 1.1) , Cache-Control优先级高

Cache-Control的取值:
* public 所有内容都将被缓存(客户端、代理服务器、CDN等)
* private 只有客户端缓存,默认值
* no-cache 客户端缓存内容,但是是否使用缓存内容,需要协商缓存确认
* no-store 所有环节都不缓存,即不使用强制缓存,也不使用协商缓存
* max-age: xxx 缓存将在多久后失效

协商缓存

缓存规则:
* 协商缓存生效,返回304,服务器告知浏览器资源未更新,浏览器再去浏览器缓存中取内容
* 协商缓存失效,返回200和结果

缓存标识:Last-Modified / If-Modified-Since 和 Etag / If-None-Match;Etag的组合的优先级更高;

协商缓存中为啥Etag的优先级比Last-Modified高,因为Last-Modified存在极特殊情况,而Etag都不存在这些情况。
* 比如对文件修改ABA的问题,从A状态修改到B再修改到A,这种情况从时间来看文件肯定是更新了,但是内容并没有变化。
* Last-Modified的时间是秒级别的,如果服务端返回的同时,在一秒内对文件做了多次更新,那么这个时间和上次的时间一样,会被误认为没有更新。
* 有些服务器不能精准获取文件的更新时间

2.HTTP 1.0 1.1 2.0区别

Http1.0

  • 无状态:服务器不跟踪不记录请求过的状态
  • 无连接:浏览器每次请求都需要建立TCP连接

Http1.1

  • 支持长连接 请求头中加入Connection: keep-alive ,如果不想长连接:Connection: false
  • 支持请求管道化

Http2.0

  • Http2.0 采用二进制格式数据帧传输数据,而非Http1.x的文本传输,更加高效
  • 多路复用,多路复用代替了http1.x中序列和阻塞机制,所有相同域名的请求都通过同一个TCP连接并发完成,针对请求并发的情况,客户端可以通过帧的标识知道属于哪个请求,通过这个技术可以解决旧版本中队头阻塞的问题,极大的提高了传输性能。
  • 服务端推送,比如请求一个html页面,服务端可以主动推送这个页面相关的js和css和图片资源,减少客户端的请求次数
  • 头部压缩,2.0对header进行压缩,并且通讯双方各cache一份header fields表,避免重复header传输。

3. Http报文及常用状态码

    /**
     * ++++++++++++++++++++       
     * +     报文首部      + ----> 请求报文首部包含: 请求行、请求首部字段、其他 
     * ++++++++++++++++++++       响应报文首部包含: 状态行、响应首部字段、其他
     * +     空行         +        
     * ++++++++++++++++++++       
     * +     报文主体      +
     * ++++++++++++++++++++
     * 
     * 请求行包括:请求方法, 请求路径path, HTTP版本  如: GET / HTTP/1.1
     * 请求首部字段:
     *    Host: xxx
     *    User-Agent: xxx
     *    Accept: xxx
     *    Accept-Language: xxx
     *    Connection: xxx
     *    If-Modified-Since: xxx
     *    If-None-Match: xxx
     *    Cache-Control: max-age=0
     * 
     * 状态行包括: HTTP版本和状态码  如: HTTP/1.1 304 Not Modified
     * 响应首部字段:
     *    Date: xxx
     *    Server: Apache
     *    Connection: close
     *    Etag: xxx
     */
     
   /**
     * Http相应状态码
     * 
     * 2xx
     * 200: 服务器正常处理了客户端发来的请求
     * 204: No Content, 服务器成功处理了客户端请求, 但没有可返回的实体。
     * 206: Partial Content, 表示客户端发起的是范围请求。相应报文中包含由Content-Range指定范围的实体内容。
     * 
     * 3xx
     * 301: 永久性重定向, 资源路径已修改
     * 302: 临时重定向
     * 303: 和302一样,只不过强制要求用get请求
     * 304: 客户端附带条件请求,但因请求未满足条件,直接返回304表示服务器资源未更新,不返回任何响应的主体部分。
     * 307: 和302一样,规定是Post不要改成Get但是实际使用时大家不遵守。而307就遵照浏览器标准,不会从Post变Get
     * 
     * 4xx
     * 400: 请求报文中存在语法错误,当错误发生时需要修改请求的内容重新发送请求
     * 401: 未认证
     * 403: 无权访问
     * 404: 服务器未找到请求的资源
     * 
     * 5xx
     * 500: 服务器内部错误
     * 503: 服务器超负载或者正在进行停机维护,现在无法处理请求。
     * 
     */

问打印结果

1. 说出下面代码的输出结果

var name = "222"

var a={
  name:"111",
  say:function(){
    console.info(this.name); 
  }
}
var fun = a.say;
fun();  //222
a.say(); // 111

var b = {
  name:"333",
  say:function(fun){
    fun();
  }
}
b.say(a.say); //111
b.say = a.say;
b.say(); //333

2. a.x 和 b.x分别分别等于多少?

function A(x) {
  this.x = x;
}
A.prototype.x = 1;
function B(x) {
  this.x = x;
}
//注意这里将B的原型指向A的实例,但是此实例没有传参数,所以实例的x属性为undefined
B.prototype = new A(); 
var a = new A(2);
var b = new B(3);
delete b.x;

//a.x = 2 b.x=undefined

3. 说出下面代码的输出结果

new Promise(resolve => {
  console.log(3)
  resolve(2)
  resolve(1)
  console.log(4)
}).then(console.log)
//3 4 2

new Promise(resolve => {
    console.log(3)
    resolve(2)
    resolve(1)
    console.log(4)
}).then(console.log)
console.log(5)
// 3 4 5 2

console.log(5)
try {
    new Promise((_, reject) => {
        console.log(3)
        reject('err msg')
    }).catch(err => {
        console.log(2)
        throw err
        console.log(4)
    })
} catch (err) {
    console.log(8)
    console.log(err)
} finally {
    console.log(9)
}
console.log(10)
// 5 3 9 10 2 Uncaught error
// 解析:try catch 只能捕获同步代码块的异常,所以try中的异步代码抛出的异常都不能捕获,比如setTimeout和Promise内部抛出的
// 先执行同步代码,输出 5 3 9 10 , 然后执行异步代码 2 ,在Promise catch中执行的代码属于异步,所以抛出的异常是不能被捕获的
// 所以会有一个Uncaught error

4. 说出下面代码的输出结果

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)
//输出1
//then里面的两个参数必须函数,第二个可以不传,如果不是函数,会给默认的函数值;
//所以第一个和第二个then里面都不是函数,会给默认值value=>value;
//这样1就被传下来了,所以最后打印1

5.事件循坏输出问题

async function async1 () {    
    console.log('1')    
    await async2()    
    console.log('AAA') 
} 
async function async2 () { 
    console.log('3')    
    return new Promise((resolve, reject) => {        
        resolve()        
        console.log('4')   
    })
} 
console.log('5')
setTimeout(() => { console.log('6') }, 0); 
async1()
new Promise((resolve) => {   
    console.log('7')    
    resolve() }
).then(() => {    
    console.log('8') 
}).then(() => {  
    console.log('9') 
}).then(() => {    
    console.log('10') 
})
console.log('11') 
// 最终结果: 5 1 3 4 7 11 8 9 AAA 10 6
// 主要是AAA的位置

算法题

1. 找到数组中每个元素后面大于自身的元素

/*
 * 给定一个数字数组,找到数组每个值后面第一个大于的它自身的元素,如果没找到,设为-1。   
 * 最后返回一个包含所有找到的值的新数组
 * 示例:
 * 输入:[1, 5, 8, 7, 2, 9, 2]
 * 输出:[5, 8, 9, 9, 9, -1, -1]
 * 要求O(n)没有思路可以暴力解
 */
 
//暴力解法 O(n^2)
function func(nums) {
  const res = [];
  for(let i=0;i<nums.length;i++){
    let find = false;
    for (let j = i+1; j < nums.length; j++) {
      if(nums[j]>nums[i]){
        find = true;
        res.push(nums[j]);
        break;
      } 
    }
    if(!find) res.push(-1);
  }
  return res;
}

//栈 O(n)
function func(nums) {
  let index = 1;
  const stack = [0];
  const len = nums.length;
  const res = new Array(len).fill(-1);
  while (index<len) {
    const preIndex = stack[stack.length-1];
    if (stack.length!==0 && nums[index]>nums[preIndex]) {
      res[stack.pop()] = nums[index];
    }else{
      stack.push(index);
      index++;
    }
  }
  return res;
}

2. 实现类的延迟方法

/*
 * class Person{
 *  eat(){}
 *  sleep(){}
 * }
 * const p = new Person();
 * p.eat("aa").sleep(1000).eat("bb")
 * .eat("cc").sleep(2000).sleep(3000).eat("dd");
 * 
 * 输出aa 等1s 输出bb cc 等5s  输出dd
 */
 
//解决方案一,Promise.then的链式调用
class Person{
  promise = Promise.resolve();
  
  eat(something){
    this.promise = this.promise.then(()=>{
      return new Promise(resolve=>{
        console.log(something);
        resolve();
      });
    });
    return this;
  }
  
  sleep(time){
    this.promise = this.promise.then(()=>{
      return new Promise(resolve=>{
        setTimeOut(()=>{
          console.log('sleep-'+time);
          resolve();
        },time);
      });
    });
    return this;
  }
}

//解决方案二
class Person{
  constructor(){
    this.task = [];
    setTimeout(()=>{
      this.next();
    });
  }
  
  eat(something){
    this.task.push(()=>{
      console.log(something);
      this.next();
    });
    return this;
  }
  
  sleep(time){
    this.task.push(()=>{
      setTimeout(()=>{
        console.log("sleep-"+time);
        this.next();
      },time);
    });
    return this;
  }
  
  next(){
    const func = this.task.shift();
    func && func();
  }
}

3.求一个Dom节点的最大深度

function maxDepth(node) {
  let depth = 0;
  const children = node.children;
  if(!children || children.length===0) return 1;
  for (let i = 0; i < children.length; i++) {
    const childMaxDepth = maxDepth(children[i]);
    if(childMaxDepth>depth) depth = childMaxDepth;
  }
  return depth + 1;
}

4.求一个无序数组的第二大元素,用O(n)的时间复杂度

function findSecondBig(nums) {
  let firstBig = nums[0],secondBig = nums[1];
  if(firstBig<secondBig){
    const temp = firstBig;
    firstBig = secondBig;
    secondBig = temp;
  }
  for (let i = 2; i < nums.length; i++) {
    if(nums[i]>firstBig){
      secondBig = firstBig;
      firstBig = nums[i];
    }else if(nums[i]<firstBig && nums[i]>secondBig){
      secondBig = nums[i];
    }
  }
  return secondBig;
}

5.实现一个函数,完成数组转换

/*
 * const arr = [
 *   ['a', 'b', 'c'],
 *   ['a', 'd'],
 *   ['d', 'e'],
 *   ['f', 'g'],
 *   ['h', 'g'],
 *   ['i']
 * ]
 * 实现一个函数,输入arr后输出如下结果:
 * [
 *  ['a', 'b', 'c', 'd', 'e'],
 *  ['f', 'g', 'h'],
 *  ['i']
 * ]
 */

function transferArray(arr) {
  const result = [];
  let tempArr = arr[0];
  for (let i = 0; i < arr.length-1; i++) {
    if(arr[i].some(item=>arr[i+1].includes(item))){
      tempArr.push(...arr[i+1]);
    }else{
      result.push([...new Set(tempArr)]);
      tempArr = arr[i+1];
    }
  }
  result.push([...new Set(tempArr)]);
  return result;
}

6.闭包问题

/* 
 * 实现一个next()
 * 例如:  var next = setup([1, 2,3, 4,5,6, 7])
 * 依次调用 next(),分别输出1,2,3,4,5,6, 7; 
 */
const arr = [1,2,3,4,5,6,7];
function setup(arr){
  let i = -1;
  return function(){
    ++i;
    return arr[i];
  }
}
const next = setup(arr);
console.log(next());

7.数组转树

const arr = [{
        id: 2,
        name: '部门B',
        parentId: 0
    },
    {
        id: 3,
        name: '部门C',
        parentId: 1
    },
    {
        id: 1,
        name: '部门A',
        parentId: 2
    },
    {
        id: 4,
        name: '部门D',
        parentId: 1
    },
    {
        id: 5,
        name: '部门E',
        parentId: 2
    },
    {
        id: 6,
        name: '部门F',
        parentId: 3
    },
    {
        id: 7,
        name: '部门G',
        parentId: 2
    },
    {
        id: 8,
        name: '部门H',
        parentId: 4
    }
]

//转换成如下格式
{
  id:2,
  name:"部门B",
  parentId:0,
  children:[
    {
      id:1,
      name:"部门A",
      parentId:2,
      children:[
        {
          id:3,
          name:"部门C",
          parentId:1,
          children:[
            {
              id:6,
              name:"部门F",
              parentId:3
            }
       		]
     		},
       {
         id:4,
         name:"部门D",
         parentId:1,
         children:[
           {
             id:8,
             name:"部门H",
             parentId:4
           }
         ]
       }
  	]
 	},
  {
    id:5,
    name:"部门E",
    parentId:2
  },
  {
    id:7,
    name:"部门G",
    parentId:2
  }
  ]
}

//代码实现
function trans(arr) {
    let result;
    const map = new Map();
    arr.forEach(item => {
        map[item.id] ={...item}; //新建对象防止破坏原有数组
    });
    for (const item of arr) {
        const parent = map[item.parentId];
        if(parent){
            //用map[item.id]而不是item,防止破坏原有数组
            (parent.children || (parent.children=[])).push(map[item.id]); 
        }else{
            result = map[item.id]; //用map[item.id]而不是item,防止破坏原有数组
        }
    }
    return result;
}

8.Promise并发控制

async function asyncPool(arr,limit,createPromiseFunc){
  const allPromises = [];
  const pendingPromises = [];
  for(const item of arr){
    const p = Promise.resolve(createPromiseFunc(item));
    allPromises.push(p);
    if(limit<arr.length){
      const thenP = p.then(()=>pendingPromises.splice(pendingPromises.indexOf(thenp),1));
      pendingPromises.push(thenP);
      if(pendingPromises.length>=limit){
        await Promise.race(pendingPromises);
      }
    }
  }
  return Promise.all(allPromises);
}

function createPromiseFunc(prop){
  console.log("开始:"+prop);
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve(prop);
      console.log("结束:"+prop);
    },1000+Math.random()*1000);
  });
}

(async ()=>{
  const res = await asyncPool([1,2,,3,4,5,6,7,8,9],2,createPromiseFunc);
  console.log(res);
})();

9.去掉字符串中第index个斜杠

const str = "abc/defg/hi/jkl/mn/opqrst";

function removeSlashes(str,index){
  if(str.indexOf("/")==-1 || index==0) return str;
  const strArray = str.split("/");
  if(index>strArray.length-1) return str;
  const firstStr = strArray.slice(0,index).join("/");
  const secondStr = strArray.slice(index).join("/");
  return firstStr + secondStr;
}
console.log(removeSlashes(str,0)); //"abc/defg/hi/jkl/mn/opqrst"
console.log(removeSlashes(str,1)); //"abcdefg/hi/jkl/mn/opqrst"

10.数组去重

//方式一
function func(arr){
    return [...new Set(arr)];
}
//方式二
function func(arr){
    return arr.filter((item,index)=>{
        return arr.indexOf(item)===index;
    });
}
//方式三
function func(arr){
    for(let i=0;i<arr.length;i++){
        for(let j=i+1;j<arr.length;j++){
            if(arr[j]===arr[i]){
                arr.splice(j,1);
                j--;
            }
        }
    }
}
//方式四
function func(arr){
    arr.sort();
    const result = [arr[0]];
    for(let i=1;i<arr.length;i++){
        if(arr[i]!==arr[i-1]){
            result.push(arr[i]);
        }
    }
    return result;
}

手写题

1. 手写instanceof

  function myInstanceOf(target, origin) {
    if (typeof origin !== "function") {
      throw new TypeError("origin must be function");
    }
    if (typeof target !== "object" || target == null) return false;
    let proto = Object.getPrototypeOf(target);
    while (proto) {
      if (proto === origin.prototype) return true;
      proto = Object.getPrototypeOf(proto);
    }
    return false;
  }

2. 手写深拷贝

function deepClone(obj,map=new WeakMap()){
    if(typeof obj!=='object' || obj==null) return obj;
    const value = map.get(obj);
    if(value) return value;
    let result = {};
    if(obj instanceof Array) result = [];
    map.set(obj,result);
    for(const key in obj){
        if(Object.hasOwnProperty.call(obj,key)){
            result[key] = deepClone(obj[key],map);
        }
    }
    return result;
}

3. 手写call方法

Function.prototype.myCall = function(obj){
    if(typeof this !== 'function'){
        thorw new TypeError('Caller must be function');
    }
    obj = obj || windown;
    const propName = Symbol();
    obj[propName] = this;
    const args = [...arguments].slice(1);
    const result = obj[propName](...args);
    delete obj[propName];
    return result;
}
//如果面试为了兼容性,不让你用扩展运算符...怎么来实现呢,下面就是答案
Function.prototype.myCall = function(obj){
    if(typeof this !== 'function'){
        throw new TypeError('caller must be function');
    }
    const args = [];
    for(let i=1;i<arguments.length;i++){
        args.push('arguments['+i+']');
    }
    //此时args类似这样args=['arguments[0]','arguments[1]']
    obj = obj || windown;
    const propName = Symbol();
    obj[propName] = this;
    const result = eval('obj[propName]('+args+')'); //这里会自动调用args.toString()方法
    delete obj[propName];
    return result;
}

练习题

1. 二分法查找,时间复杂度O(logn)

  function binarySearch(arr, target) {
    let start = 0,end = arr.length - 1;
    while (start <= end) {
      let mid = Math.floor((start + end) / 2);
      if (arr[mid] > target) {
        end = mid - 1;
      } else if (arr[mid] < target) {
        start = mid + 1;
      } else {
        return mid;
      }
    }
    return -1;
  }

2. 冒泡排序

  function bubbleSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
      let isSwap = false;
      for (let j = 0; j < arr.length - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
          const temp = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = temp;
          isSwap = true;
        }
      }
      if (!isSwap) break;
    }
  }