2024 JS 面试手写代码 31 题

307 阅读14分钟

备注:

//变更函数调用者示例
function foo() {
    console.log(this.name)
}

// 测试
const obj = {
    name: '写代码像蔡徐抻'
}
obj.foo = foo   // 变更foo的调用者
obj.foo()       // '写代码像蔡徐抻'

0. 手写 _const

实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty() 接收三个参数:

Object.defineProperty(obj, prop, desc)

writable: false

function _const(key, value) {    
    const desc = {        
        value,        
        writable: false    
    }    
    Object.defineProperty(window, key, desc)
}
    
_const('obj', {a: 1})   //定义obj
obj.b = 2               //可以正常给obj的属性赋值
obj = {}                //无法赋值新对象

1. 手写call()

语法:function.call(thisArg, arg1, arg2, ...)

Function.prototype.myCall = function(thisArg, ...args) {
    const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    thisArg = thisArg || window    // 若没有传入this, 默认绑定window对象
    thisArg[fn] = this              // this指向调用call的对象,即我们要改变this指向的函数
    const result = thisArg[fn](...args)  // 执行当前函数
    delete thisArg[fn]              // 删除我们声明的fn属性
    return result                  // 返回函数执行结果
}

//测试
foo.myCall(obj)     // 输出'写代码像蔡徐抻'

2. 手写apply()

语法:func.apply(thisArg, [argsArray])

Function.prototype.myApply = function(thisArg, args) {
    const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    thisArg = thisArg || window    // 若没有传入this, 默认绑定window对象
    thisArg[fn] = this              // this指向调用call的对象,即我们要改变this指向的函数
    const result = thisArg[fn](...args)  // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,但在调用原函数时,依然要展开参数数组。可以对照原生apply(),原函数接收到展开的参数数组)
    delete thisArg[fn]              // 删除我们声明的fn属性
    return result                  // 返回函数执行结果
}

//测试
foo.myApply(obj, [])     // 输出'写代码像蔡徐抻'

3. 手写bind()

语法: function.bind(thisArg, arg1, arg2, ...)

  1. bind()除了this还接收其他参数,bind()返回的函数也接收参数,这两部分的参数都要传给返回的函数
  2. new会改变this指向:如果bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例
  3. 需要保留原函数在原型链上的属性和方法
Function.prototype.myBind = function (thisArg, ...args) {
    var self = this
    // new优先级
    var fbound = function () {
        self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
    }
    // 继承原型上的属性和方法
    fbound.prototype = Object.create(self.prototype);

    return fbound;
}

//测试
const obj = { name: '写代码像蔡徐抻' }
function foo() {
    console.log(this.name)
    console.log(arguments)
}

foo.myBind(obj, 'a', 'b', 'c')()    //输出写代码像蔡徐抻 ['a', 'b', 'c']

4. 手写防抖函数

防抖,即 短时间内大量触发同一事件,只会执行一次函数。 实现原理为 设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。

function debounce(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait)
    }
}

5. 手写节流函数

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间才能执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器

function throttle(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null
                func.apply(context, args)
            }, wait)
        }

    }
}

6. 手写 reduce

Array.prototype.reduce = function(fn, init) {
    var arr = this   // this就是调用reduce方法的数组
    var total =  init || arr[0]   // 有初始值使用初始值
    
    // 有初始值的话从0遍历, 否则从1遍历
    for (var i = init ? 0 : 1; i < arr.length; i++) {
        total = fn(total, arr[i], i , arr)
    } 
    return total
}

var arr = [1,2,3];
console.log(arr.reduce((prev, item) => prev + item, 10)) ;

7. 手写 flat 数组扁平化

对于树状结构的数据,最直接的处理方式就是递归

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  let result = []
  for (const item of arr) {
    item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
  }
  return result
}

flat(arr) // [1, 1, 2, 1, 2, 3]

8. 手写 getType

const getType = data => {
  // 先判断该数据是否是基本数据类型,成立则将其类型返回
  if (typeof data !== 'object') return typeof data
  // 获取该数据的数据类型
  const data_type = Object.prototype.toString.call(data) // "[object ...]" 例如:array --> "[object Array]"
  // 将重要数据解析后转换为小写返回
  return data_type.replace(/[|]|(object )|]/g, '').toLowerCase()
}

console.log(getType(Symbol.for(1)))   // symbol
console.log(getType(null))  // null
console.log(getType(undefined)) // undefined
console.log(getType([])) // array
console.log(getType({})) // object
console.log(getType(() => {})) // function
console.log(getType(Promise.resolve())) // promise
console.log(getType(new Set())) // set
console.log(getType(new Map())); // map

8. 寄生式组合继承

8.1 原型链继承

缺点:

1 由于所有 Child 实例原型都指向同一个 Parent 实例,

因此对某个 Child 实例的父类引用类型变量修改会影响所有的Child实例

2 在创建子类实例时无法向父类构造传参, 即没有实现 super() 的功能

function Child() {}

Child.prototype = new Parent()
Child.prototype.constructor = Child 

8.2 构造函数继承:

构造函数继承: 即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参

缺点:继承不到父类原型上的属性和方法

function Child() {
    Parent.call(this, 'zhangsan')   // 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上
}

8.3 组合式继承

缺点:

每次创建子类实例都执行了两次构造函数(Parent.call()和new Parent()),

虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,不优雅

function Child() {
    // 构造函数继承
    Parent.call(this, 'zhangsan') 
}
//原型链继承
Child.prototype = new Parent()
Child.prototype.constructor = Child

8.4 寄生组合式继承

为了解决构造函数被执行两次的问题, 我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}

function Child() {
    // 构造函数继承
    Parent.call(this, 'zhangsan') 
}

//原型链继承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype)  // 将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child

//测试
const child = new Child()
const parent = new Parent()
child.getName()                  // ['zhangsan']
parent.getName()                 // [undefined], 构造时没有传

到这里我们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承,是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承

9. ES6 class extends 继承

// class 相当于es5中构造函数
// class 中定义方法时,前后不能加 function,全部定义在 class 的 protopyte 属性中
// class 中定义的所有方法是不可枚举的
// class 中只能定义方法,不能定义对象,变量等
// class 和方法内默认都是严格模式
// es5 中 constructor 为隐式属性

class People{
  constructor(name='wang',age='27'){
    this.name = name;
    this.age = age;
  }
  eat(){
    console.log(`${this.name} ${this.age} eat food`)
  }
}
//继承父类
class Woman extends People{ 
   constructor(name = 'ren',age = '27'){ 
     //继承父类属性
     super(name, age); 
   } 
    eat(){ 
     //继承父类方法
      super.eat() 
    } 
} 
let wonmanObj=new Woman('xiaoxiami'); 
wonmanObj.eat();

// es5 继承先创建子类的实例对象,然后再将父类的方法添加到this上( Parent.apply(this) )。 
// es6 继承是使用关键字 super 先创建父类的实例对象 this,最后在子类 class 中修改 this 。

10. new 的实现

  • 一个继承自 实例 prototype 的新对象被创建。
  • 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
  • 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。
  • 一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤
function Ctor(){
    ....
}

function myNew(ctor,...args){
    if(typeof ctor !== 'function'){
      throw 'myNew function the first param must be a function';
    }
    // 1 创建一个继承自 ctor.prototype 的新对象
    var newObj = Object.create(ctor.prototype);
    // 2 将构造函数 ctor 的 this 绑定到 newObj 中
    var ctorReturnResult = ctor.apply(newObj, args);
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    // 3 返回这个绑定后的对象函数,或者新对象
  	if(isObject || isFunction){
        return ctorReturnResult;
    }
    return newObj;
}

let c = myNew(Ctor);

11. instanceof 的实现

  • instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。
  • instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
  • 不能检测基本数据类型,在原型链上的结果未必准确,不能检测null,undefined
  • 实现:遍历左边变量的原型链,直到找到右边变量的 prototype,如果没有找到,返回 false
function myInstanceOf(A,B){
    let left = A.__proto__;
    let right = B.prototype;
    while(true){
        if(left == null) return false;
        if(left == right) return true;
        left = left.__proto__;
    }
}

12. Object.create() 的实现

  • Object.create() 会将参数对象作为一个新创建的空对象的原型, 并返回这个空对象
//简略版
function myCreate(obj){
    // 新声明一个函数
    function C(){};
    // 将函数的原型指向obj
    C.prototype = obj;
    // 返回这个函数的实力化对象
    return new C()
}

13. Object.assign 实现

Object.assign2 = function(target, ...source) {
    if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object')
    }
    let ret = Object(target) 
    source.forEach(function(obj) {
        if (obj != null) {
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    ret[key] = obj[key]; // 新对象上挂载后面的属性
                }
            }
        }
    })
    return ret;
}

14. XMLHttpRequest 实现

function ajax(url,method,body,headers){
    return new Promise((resolve,reject)=>{
        let req = new XMLHttpRequest();
        req.open(methods,url);
        for(let key in headers){
            req.setRequestHeader(key,headers[key])
        }
        req.onreadystatechange(()=>{
            if(req.readystate == 4){
                if(req.status >= '200' && req.status <= 300){
                    resolve(req.responeText)
                }else{
                    reject(req)
                }
            }
        })
        req.send(body)
    })
}

15. deepclone 深拷贝

  • 判断类型,正则和日期直接返回新对象
  • 空或者非对象类型,直接返回原值
  • 考虑循环引用,判断如果hash中含有直接返回hash中的值
  • 新建一个相应的new obj.constructor加入hash
  • 遍历对象递归(普通key 和 key是symbol 情况)
function deepClone(obj,hash = new WeakMap()){
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(obj === null || typeof obj !== 'object') return obj;
    //循环引用的情况
    if(hash.has(obj)){
        return hash.get(obj)
    }
    //new 一个相应的对象
    //obj为Array,相当于new Array()
    //obj为Object,相当于new Object()
    let constr = new obj.constructor();
    hash.set(obj,constr);
  
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            constr[key] = deepClone(obj[key],hash)
        }
    }
    //考虑symbol的情况
    let symbolObj = Object.getOwnPropertySymbols(obj)
    for(let i=0;i<symbolObj.length;i++){
        if(obj.hasOwnProperty(symbolObj[i])){
            constr[symbolObj[i]] = deepClone(obj[symbolObj[i]],hash)
        }
    }
    return constr;
}

16. 函数柯里化 curry

柯里化是指这样一个函数(假设叫做 Curry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。

function curry(fn,...args){
  let fnLen = fn.length;
  let argsLen = args.length;
  //对比函数的参数和当前传入参数
  //若参数不够就继续递归返回curry
  //若参数够就调用函数返回相应的值
  if(fnLen > argsLen){
    return function(...arg2s){
      return curry(fn,...args,...arg2s)
    }
  }else{
    return fn(...args)
  }
}


function sumFn(a,b,c){return a+ b + c};
let sum = curry(sumFn);
sum(2)(3)(5)//10
sum(2,3)(5)//10

16. 动态参数个数 add

function add (...params) {
  const proxy = (...args) => {
    params = params.concat(args)
    return proxy
  }

  proxy.toString = () => params.reduce((prev, next) => prev + next)
  return proxy
}

// alert(add(1)(2)(3))
// alert(add(1,2,3)(4))

// alert(add(1)(2)(3)())
// alert(add(1,2,3)(4)())

17. 闭包实现每隔一秒打印 1,2,3,4

for (var i=1; i<5; i++) {
  (function (i) {
    setTimeout(() => console.log(i), 1000*i)
  })(i)
}

18. jsonp 实现

const jsonp = function (url, data) {
    return new Promise((resolve, reject) => {
        // 初始化url
        let dataString = url.indexOf('?') === -1 ? '?' : ''
        let callbackName = `jsonpCB_${Date.now()}`
        url += `${dataString}callback=${callbackName}`
        if (data) {
            // 有请求参数,依次添加到url
            for (let k in data) {
                url += `${k}=${data[k]}`
            }
        }
        let jsNode = document.createElement('script');
        jsNode.src = url
        // 触发callback,触发后删除 js标签 和 绑定在 window上的 callback
        window[callbackName] = result => {
            delete window[callbackName];
            document.body.removeChild(jsNode);
            if (result) {
                resolve(result)
            } else {
                reject('没有返回数据')
            }
        }
        // js加载异常的情况
        jsNode.addEventListener('error', () => {
            delete window[callbackName];
            document.body.removeChild(jsNode);
            reject('JavaScript资源加载失败');
        }, false);
        // 添加js节点到document上时,开始请求
        document.body.appendChild(jsNode)
    })
}
jsonp('http://192.168.0.103:8081/jsonp', {
    a: 1,
    b: 'heiheihei'
})
.then(result => {
    console.log(result)
})
.catch(err => {
    console.error(err)
})

19. 数组的随机排序

function getRandom(min, max) {
  return Math.floor(Math.random() * (max - min)) + min   
}

20. 事件侦听器

const EventUtils = {
  // 视能力分别使用 dom0 || dom2 || IE 方式 来绑定事件
  // 添加事件
  addEvent: function(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  },
  // 移除事件
  removeEvent: function(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
  },
  // 获取事件目标
  getTarget: function(event) {
    return event.target || event.srcElement;
  },
  // 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
  getEvent: function(event) {
    return event || window.event;
  },
  // 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  },
  // 取消事件的默认行为
  preventDefault: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  }
};

23. imgLazyLoad 图片懒加载

let imgList = [...document.querySelectorAll('img')];
let length = imgList.length;

const imgLazyLoad = function() {
    let count = 0;
    return (function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++;
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        });
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index));
    })()
}

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad);

24. lazyMan

class _LazyMan {
  constructor (name) {
    this.name = name
    this.queue = []
    this.queue.push(() => {
      console.log(`Hi This is ${name}`)
      this.next()
    })
    setTimeout(() => {
      this.next()
    })
  }

  next () {
    const task = this.queue.shift()
    task && task()
  }

  sleep (delay) {
    const task = () => {
      setTimeout(() => {
        console.log(`Wake up after ${delay}`)
        this.next()
      }, 1000 * delay)
    }
    this.queue.push(task)
    return this
  }

  eat (str) {
    const task = () => {
      console.log(`Eat ${str}~`)
      this.next()
    }
    this.queue.push(task)
    return this
  }

  sleepFirst (delay) {
    const task = () => {
      setTimeout(() => {
        console.log(`Wake up after ${delay}`)
        this.next()
      }, 1000 * delay)
    }
    this.queue.unshift(task)
    return this
  }
}

function LazyMan (name) {
  return new _LazyMan(name)
}

const p = LazyMan('Hank').sleep(2).eat('dinner')
// const p = LazyMan('Hank').eat('dinner').eat('supper')
// const p = LazyMan('Hank').eat('supper').sleepFirst(2)

25. dom2Json

function dom2Json(domtree) {
  let obj = {};
  
  obj.name = domtree.tagName;
  
  obj.children = [];
  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
  
  return obj;
}

dom2Json(domTree());

26. Json2dom

function render (obj) {
  let dom = document.createElement(obj.tag)
  obj.attrs && Object.keys(obj.attrs).forEach(key => {
    dom.setAttribute(key, obj.attrs[key])
  })
  obj.children.length && obj.children.forEach(child => {
    dom.appendChild(render(child))
  })
  return dom
}

let obj = {
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

console.log(render(obj))

27. Object.is()

Object.is()方法判断两个值是否为同一个值

== 运算符在判断相等前对两边的变量(如果它们不是同一类型)进行强制转换,而 Object.is 不会强制转换两边的值。
与 === 的差别是它们对待有符号的零和 NaN 不同

(例如,=== 运算符(也包括 == 运算符)将数字 -0 和 +0 视为相等,而将 Number.NaN 与 NaN 视为不相等)

function is(x, y) {
  if (x === y) {
    // 针对+0 不等于 -0的情况
    return x !== 0 || 1 / x === 1 / y
  } else {
    // 针对NaN的情况
    return x !== x && y !== y
  }
}

28. compose 组合函数

如果一个函数需要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并一个函数

// redux 中 compose实现
function compose(...fns) {
  return fns.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  )
}
// 理解版
function compost2(...fns) {
  return function (args) {
    for (let i = fns.length - 1; i >= 0; i--) {
      args = fns[i](args)
    }
    return args
  }
}

29. 实现 Ajax

function ajax(url, methods, body, headers) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open(methods, url)
    for (let key in headers) {
      xhr.setRequestHeader(key, headers[key])
    }
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText)
      } else {
        reject(new Error(xhr.responseText))
      }
    }
    xhr.send()
  })
}

30. lodash _.get()

const get = (data, path, defaultValue = 0) => {
    let result = data;
    // 'a[0].b.c' ==> 'a.0.b.c' ==> ['a', '0', 'b', 'c']
    const regPath = path.replace(/[(\d+)]/g, '.$1').split('.');
    for(const path of regPath){
        // 用Object包一层,因为null与undefined取属性会报错
        result = Object(result)[path];
        if(result == null){
            return defaultValue;
        }
    }

    return result;
}

const obj = { 
    selector: { 
        to: { 
            toutiao: 'FE coder' 
        } 
    }, 
    target: [1, 2, { name: 'byted' }] 
}
// 运行代码
console.log(get(obj, 'selector.to.toutiao'))
console.log(get(obj, 'target[0]'))
console.log(get(obj, 'target[2].name'))

//  输出结果:
// ['FE coder', 1, 'byted']

31 eventBus

/*
 * ReactJs或Vue大型应用,常见跨组件(非父子组件场景)通信。请简单实现一个事件总线类MessageCenter
 * 实现subcribe注册函数,unsubscribe移除函数,publish发布则触发顺序执行
 */
const MessageCenter = {
    // 补全代码实现
    eventBus : {},
    subscribe: function(eventName, eventCallback, thisArg) {
        let handlers = this.eventBus[eventName];
        if (!handlers) {
            handlers = [];
        }

        handlers.push({
            eventCallback,
            thisArg
        })

        this.eventBus[eventName] = handlers;

        return eventCallback;
    },
    unsubscribe: function(eventName, eventCallback) {
        let handlers = this.eventBus[eventName];
        if (!handlers) {
            return;
        }

        const newHandlers = [...handlers];

        for (let index = 0; index < newHandlers.length; index++) {
            let handle = newHandlers[index];
            if (handle.eventCallback === eventCallback) {
                const handleIndex = handlers.indexOf(handle);
                handlers.splice(handleIndex, 1);
            }
        }
    },
    publish: function(eventName, ...playLoad) {
        let handlers = this.eventBus[eventName];
        if (!handlers) {
            return;
        }

        handlers.forEach((handle) => {
            handle.eventCallback.apply(handle.thisArg, playLoad);
        })
    }
}
 
/* 场景案例 1 */
let listener1 = MessageCenter.subscribe('event', (data) => {
    console.log('event listener1 run', data);
})
let listener2 = MessageCenter.subscribe('event', (data) => {
    console.log('event listener2 run', data);
})
 
MessageCenter.publish('event', 'AAA');
 
// 期望输出
// event listener1 run AAA
// event listener2 run AAA
 
/* 场景案例 2 */
let listener3 = MessageCenter.subscribe('event', (data) => {
    console.log('event listener3 run', data);
});
let listener4 = MessageCenter.subscribe('event', (data) => {
    console.log('event listener4 run', data);
});
 
MessageCenter.unsubscribe('event', listener3);
MessageCenter.publish('event', 'BBB');

// 期望输出
// event listener4 run BBB

1. 手写 冒泡排序

function bubbleSort(arr){
  for(let i = 0; i < arr.length; i++) {
    for(let j = 0; j < arr.length - i - 1; j++) {
      if(arr[j] > arr[j+1]) {
        let temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
      }
    }
  }
  return arr
}

2. 手写 快速排序

快排基本步骤:

  1. 选取基准元素
  2. 比基准元素小的元素放到左边,大的放右边
  3. 在左右子数组中重复步骤一二,直到数组只剩下一个元素
  4. 向上逐级合并数组
function quickSort(arr) {
    if(arr.length <= 1) return arr          //递归终止条件
    const pivot = arr.length / 2 | 0        //基准点
    const pivotValue = arr.splice(pivot, 1)[0]
    const leftArr = []
    const rightArr = []
    arr.forEach(val => {
        val > pivotValue ? rightArr.push(val) : leftArr.push(val)
    })
    return [ ...quickSort(leftArr), pivotValue, ...quickSort(rightArr)]
}

3. 手写 归并排序

归并排序和快排的思路类似,都是递归分治,区别在于快排边分区边排序,而归并在分区完成后才会排序

function mergeSort(arr) {
    if(arr.length <= 1) return arr		//数组元素被划分到剩1个时,递归终止
    const midIndex = arr.length/2 | 0
    const leftArr = arr.slice(0, midIndex)
    const rightArr = arr.slice(midIndex, arr.length)
    return merge(mergeSort(leftArr), mergeSort(rightArr))	//先划分,后合并
}

//合并
function merge(leftArr, rightArr) {
    const result = []
    while(leftArr.length && rightArr.length) {
    	leftArr[0] <= rightArr[0] ? result.push(leftArr.shift()) : result.push(rightArr.shift())
    }
    while(leftArr.length) result.push(leftArr.shift())
    while(rightArr.length) result.push(rightArr.shift())
    return result;
}

4. 手写 堆排序

堆排序的流程:

  1. 初始化大(小)根堆,此时根节点为最大(小)值,将根节点与最后一个节点(数组最后一个元素)交换
  2. 除开最后一个节点,重新调整大(小)根堆,使根节点为最大(小)值
  3. 重复步骤二,直到堆中元素剩一个,排序完成
// 堆排序
const heapSort = array => {
        // 我们用数组来储存这个大根堆,数组就是堆本身
	// 初始化大顶堆,从第一个非叶子结点开始
	for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
		heapify(array, i, array.length);
	}
	// 排序,每一次 for 循环找出一个当前最大值,数组长度减一
	for (let i = Math.floor(array.length - 1); i > 0; i--) {
		// 根节点与最后一个节点交换
		swap(array, 0, i);
		// 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
		heapify(array, 0, i);
	}
	return array;
};

// 交换两个节点
const swap = (array, i, j) => {
	let temp = array[i];
	array[i] = array[j];
	array[j] = temp;
};

// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
const heapify = (array, i, length) => {
	let temp = array[i]; // 当前父节点
	// j < length 的目的是对结点 i 以下的结点全部做顺序调整
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
		if (j + 1 < length && array[j] < array[j + 1]) {
			j++; // 找到两个孩子中较大的一个,再与父节点比较
		}
		if (temp < array[j]) {
			swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
			i = j; // 交换后,temp 的下标变为 j
		} else {
			break;
		}
	}
}

5. 归并、快排、堆排有何区别

排序时间复杂度(最好情况)时间复杂度(最坏情况)空间复杂度稳定性
快速排序O(nlogn)O(n^2)O(logn)~O(n)不稳定
归并排序O(nlogn)O(nlogn)O(n)稳定
堆排序O(nlogn)O(nlogn)O(1)不稳定

在实际运用中, 并不只使用一种排序手段, 例如V8的 Array.sort() 就采取了当 n<=10 时, 采用插入排序, 当 n>10 时,采用三路快排的排序策略。