备注:
//变更函数调用者示例
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, ...)
- bind()除了this还接收其他参数,bind()返回的函数也接收参数,这两部分的参数都要传给返回的函数
- new会改变this指向:如果bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例
- 需要保留原函数在原型链上的属性和方法
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. 手写 快速排序
快排基本步骤:
- 选取基准元素
- 比基准元素小的元素放到左边,大的放右边
- 在左右子数组中重复步骤一二,直到数组只剩下一个元素
- 向上逐级合并数组
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. 手写 堆排序
堆排序的流程:
- 初始化大(小)根堆,此时根节点为最大(小)值,将根节点与最后一个节点(数组最后一个元素)交换
- 除开最后一个节点,重新调整大(小)根堆,使根节点为最大(小)值
- 重复步骤二,直到堆中元素剩一个,排序完成
// 堆排序
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 时,采用三路快排的排序策略。