javascript简单算法

59 阅读9分钟

实现一些已有算法的第一步是知道这个方法怎么用,第二步是实现原理,第三步是实现,第四步是测试实现的算法是否与原来算法一致,这个算法要干啥,入参出参数什么,接下来的js算法中将从这四步实现

实现 new

step1
箭头函数只针对非箭头函数,箭头函数不能使用new,因为箭头函数没有自身的this和prototype属性,箭头函数不绑定自己的this,他会捕获上下文中的this,值作为自己的 this。在使用 new 操作符时,会创建一个新的对象,并将构造函数内部的 this 绑定到这个新对象。然而,箭头函数没有自己的 this,因此无法与 new 操作符一起使用。使用 new 操作符创建对象时,新的对象会继承构造函数的原型属性。然而,箭头函数没有 prototype 属性,因此不能通过 new 操作符使用。

//第一种没有显示返回值
const test = function(){
    console.log(this)//test{}
    this.a = 444;
}
const newObj = new test()
console.log(newObj)//{ a: 444 }
//第二种有显示返回值
const returnTest = function(){
    this.b = 333;
    return {c:3}
}
const returnNewObj = new test()
console.log(returnNewObj)//{c:3}

step2:

  1. 创建一个新对象
  2. 将构造函数的this绑定到新对象上
  3. 执行构造函数代码
  4. 返回新对象

step:3 实现

    function myNew(constructor,...args){
        //1、创建一个新对象,为什么使用Object.create()不适用{},
        //因为Object.create()可以给生成对象指定__proto__ = constructor.prototype,继承constructor原型
        const obj = Object.create(constructor.prototype)
        //2、绑定this,并执行代码
        const result = constructor.apply(obj,args)
        //3、返回新对象
        return result instanceof Object? result:obj 
    }

step3:使用myNew测试开始的示例

const test = function(){
    console.log(this)//test{}
    this.a = 444;
}
const newObj = myNew(test)
console.log(newObj)//{ a: 444 }
//第二种有显示返回值
const returnTest = function(){
    this.b = 333;
    return {c:3}
}
const returnNewObj = myNew(returnTest)
console.log(returnNewObj)//{c:3}

实现call、apply、bind

问题:为什么会有call、apply和bind
普通的函数(非箭头函数)中的this的指向是在函数被调用时的调用者,也就是执行上下文创建时确定
step1:call的使用方法

function greet(greeting, punctuation){
    console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
greet.call(person, 'Hello', '!'); // 输出:Hello, Alice!

step2:实现原理

    func.call(ctx,arg1,...)

    //等价于以下代码
    ctx.fn=func;
    ctx.fn(arg1,....)

step3:实现

    //函数的调用,需要添加在原型上
    Function.prototype.myCall=function(context,...args){
    //对this进行类型判断,如果不是function类型,就报错
    //this应该指向的是调用myCall函数的对象(function也属于对象object类型)
    //因为myCall的调用模式是上文提到的‘方法调用模式’
    if(typeof this != 'function'){
        throw new TypeError('type error')
    }
 
    // 不传的话,默认上下文是window
    var context = context || window
    
   // 假如context上面有fn属性的时候,会出现冲突
   // 所以当context上面有fn属性的时候,要先保存一下
    var temp = null
    if (context.fn) {
        temp = context.fn
    }
    // 给context创建一个fn属性,并将值设置为需要调用的函数(即this)
    context.fn = this
    //调用函数
    const res = context.fn(...args)
 
    // 删除context对象上的fn属性
    if (temp) {
        context.fn = temp
    } else {
        delete context.fn
    }
 
    // 返回运行结果
    return res
}

step4:测试

    greet.myCall(person, 'Hello', '!'); // 输出:Hello, Alice!

call和apply的传参方式不同其他都相同

Function.prototype.myApply=function(context) {
    if( typeof this !== 'function' ){
        throw new TypeError('Error');
    }
    context = context || window;
    let temp = null;
    if(context.fn){
        temp = context.fn;
    }else{
        context.fn = this;
    }
    context.fn = this;
    console.log(arguments);
    const result = arguments[1] ? context.fn(...arguments[1]) : context.fn();
    if(temp){
        context.fn = temp;
    }else{
        delete context.fn;
    }
    return result;
}

step4:测试

let num=1;
let obj={
    num:2,
    fn:'this is obj.fn'
}
 
function test(a,b,c,d){
    console.log(this.num,'test参数:',a,b,c,d)
}
 console.log(obj,'obj')
// 调用myCall函数
test.myApply(obj,[1,2,3,4])
 
// 检查obj本身的fn是否被修改
console.log(obj.fn)

bind与call和apply的区别是bind不会立即执行只会更改this的指向,在绑定时可以传递参数,

    Function.prototype.myBind = function (context,...args0){
        //首先判断this是否是函数
        if(typeof this != 'function'){
         throw new TypeError('type Error');
        }
        let fn = this;
        
        return function Fn(...args){
            if(this instanceof Fn){
                return new fn(...args0,...args)
            }else{
                return fn.call(context, ...args0, ...args)
            }
        }
    }

step4:测试

function Point(x, y) {
    this.x = x;
    this.y = y;
  }
  // 情况1:正常调用bind函数
  var testObj = {};
  var YAxisPoint = Point.myBind(testObj, 0 );
  YAxisPoint(1)
  console.log(testObj)
   
  // 情况2:bind返回函数作为构造函数
  // 此时之前绑定的指向testObj的this会失效,会重新指向新的对象实例,但是参数会继续有效
  let newObj=new YAxisPoint(2);
  console.log('newObj',newObj)

实现instanceof

instanceof 用来判断 A的__proto__是不是B的prototype,只用于检查对象,不能检查基本类型,检查类型需要配合typeof

step1:

 const d = new Date();
 d instanceof Object//true
 const string = '';
 string instanceof String;//false
 const stringObj = new String('string Object');
 stringObj instanceof String//true

step2:

instanceof 在原型连上查找 A.proto == B.prototype

step3:

    function myInstanceof(obj,Constructor){
        while(obj){
            //由制造原型链的new决定的  new中 obj = Object.create(Constructor.prototype)
            if(obj.__proto__ == Constructor.prototype) return true;
            obj = obj.__proto__;//去obj的下一级查找
        }
        return false
    }

step4:

    myInstanceof({},Object);
    

实现Object.keys()、Object.values(),Object.entries()

Object是不可迭代的,因此不能使用for of遍历,需要使用 for in 实现,

实现Object.Keys()

    //实现Object.keys()
    //入参是一个对象
    function objectKeys(obj){
        //出参事一个数组
        const keys = [];
        for(const item in obj){
            if(obj.hasOwnProperty(item)){
                keys.push(item)
            }
        }
        return keys
    }

实现Object.values

    function objectValues(obj){
        const values = [];
        for(const item in obj){
             if(obj.hasOwnProperty(item)){
                keys.push(obj[item])
            }
        }
        return values;
    }

实现Object.entries()

    function objectEntries(){
        const entries = [];
        for(const item in obj){
            if(obj hasOwnPrototype(item)){
                entries.push([item,obj[item]])
            }
        }
        return entries
    }

防抖函数

防抖函数是合并多次执行,比如延时时间为5秒,如果5秒内有函数触发就合并为最后一次执行,如果5秒没有函数触发则执行

    function debounce(fn,wait,imadiate){
        //第三个参数为是否立即执行
        let timer = null;
        //为什么返回函数,有利于函数调用及传递参数
        return function(){
            //下面参数定义及使用apply为了兼容非箭头函数this指向
            let args = arguments,context = this;
            //每次进来销毁定时器,重新计时,最后一次触发延时执行
            if(timer){cleatTimeout(timer)}
            if(imadiate&&!timer){
                fn.apply(context,args)
            }
            timer = setTimeout(()=>{
                fn.apply(context,args);
                //执行成功后清空定时器
                clearTimeout(timer)
            },wait)
        }
    }

测试下

     function fn(e){
       console.log(e,this.a);
    }
    const obj={a:1,b:debounce(fn,1000,true)}
    document.onmousemove = function(e){
        obj.b(e)
    }

节流

节流函数是每隔固定的时间执行一次函数,比如试图重绘

    funtion throttles(fn,interval,imadiate){
        let timer = null
        //返回值
      return function(){
          const args = arguments,context = this;
          //开关,防止每次进来都创建一个延时函数
          //只有执行过后重新创建延时函数
          if(!timer){
              timer = setTime(()=>{
                  fn.apply(context,args);
                  timer = null;
              },interval)
          }
      }
    }

浅拷贝和深拷贝

浅拷贝和深拷贝是针对引用类型的,基本类型存放在栈中,而引用对象既存在于栈中也存在于堆中,应用兑现对象的对像名和在堆中的地址存在栈中
浅拷贝是对象的一层拷贝,对象的属性是基本类型则直接复制,如果是对象则拷贝对象的地址

    function shallowCopy(obj){
    const result = {};
        if(Object.prototype.toString.call(obj)!="[object Object]"){
            throw new TypeError()
        }
        for(item in obj){
            if(obj.hasOwnProperty(item)){
            result[item] = obj[item]
            }
        }
        return result
    }

深拷贝

    function deepClone(obj){
        const result={};
        for(item in obj){
            if(obj.hasOwnProperty(item)){
                if(typeof obj[item] == 'object'){
                    result[item] = deepClone(obj[item])
                }else{
                    result[item] = obj[item]
                }
             }
        }
        return result
    }

二叉树的深度优先遍历

递归版

    function deepTreeTraveral(tree){
        if(!tree) return
        deepTreeTraveral(tree.left);
        console.log(tree.val)
        deepTreeTraveral(tree.right);
    }

迭代版

    function iteratorTreeTraveral(tree){
        let stask = [],current = tree;
        //判断栈或当前节点是否为true
        while(task.length>0||current){
            //左子树有值一直遍历
            while(current){
               stash.push(current);
               current = current.left;
            }
            //输出值及遍历当前节点的右子树
            current = stash.pop();
            console.log(current.val);
            current = current.right;
        }
    }

柯里化函数

curry函数是接受部分参数,参数分批接收,并返回一个函数的函数 例如是先add(1)(2)(3)(4)

    function add(x){
        //在作用于中记录第一次传入的参数
        let sum = x;
        //接受剩余的参数并计算, 
        let tmp = function(y){
            sum = sum + y;
            除第一次外剩余执行时的执行函数
            return tmp
        }
        //可以是任何函数,最后用于获取值
        tmp.toString = ()=>sum
        //第一次执行返回tmp函数
        return tmp
    }

数组去重

测试用例

    const arr = [6, 6, '6', true, true, 'true','true', NaN, NaN,'NaN', undefined, undefined, null,null];

    console.log(arrayUnique(arr));
    // [ 6, '6', true, 'true', NaN, 'NaN', undefined, null ]

实现

    function unique(arr){
        let uniqueArry = [];
        uniqueArry = [...new Set(arr)]
        //或者
        uniqueArry = Arry.from(new Set(arr))
        return uniqueArry
    
    }
   //原始写法,但是没有以上两种效率快,内存
   function uniqueMap(arr){
       const uniqueArr = [];
       const map = new Map()
       arr.forEach(item=>{
           if(!map.has(item)){
               uniqueArr.push(item);
               map.set(item,true)
           }
       })
       return uniqueArr;
   }

数组扁平化

    //第一种:使用数组自带的方法flat在不知道有多少层嵌套时flat的入参为Infinity
    arr = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
    arr.flat(Infinity);
    //第二种:使用正则表达式
    //先使用JSON.stringify把数组转为字符,
    //但是不能使用toString(),toString会直接返回一维数组,但是会把数字转为字符串,对arr[number]不友好
    JSON.stringify(arr).replace(/[\[|\]]/,'')
    //第三种使用stack
    const flat = (arr)=>{
    const result = [];
    //[...arr]防止只有以为数组
    const stack = [...arr];
    // const current = stack.shift()
    while(stack.length>0){
        //保证顺序
        const current = stack.shift()
        if(Array.isArray(current)){
            //推入栈中
            stack.unshift(...current)
        }else{
            result.push(current)
        }
    }
    return result
}
// 第四种方法 使用reduce
    const reduceFlat=(arr)=>{
    //reduce 第一个参数是执行函数,第二个参数是初始值,
    //函数中的上一次返回的结果,第二个参数是当前值
    return arr.reduce((result,current)=>{
        if(Array.isArray(current)){
            return [...result,...reduceFlat(current)]
        }else{
            return [...result,current]
        }
    },[])
}
    

函数组合

是函数式编程中的概念,组合函数执行,把函数组合起来使用,通过以下示例来解释

    function compose(f,g){
        return function(x){
            return f(g(x))
        }
    }

f、g是函数,x是他们之间通过管道传输的值
组合看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合产下一个崭新的函数。 compose是需要自己实现或者使用第三方库

// 不确定参数个数的compose函数
    function composePro(){
        const args = [].slice.call(arguments);
        return args.reduce((acc,curr)=>{
            return (...args)=>{
                return acc(curr(...args))
            }
        })
    }
    let toUpperCase = (x) => x.toUpperCase();
    let exclaim = (x) => x + ' ao ao ao!!!';
    let nameHead = (x) => 'neverMore ' + x;
    console.log('composePro(exclaim, nameHead): ', composePro(exclaim, nameHead));
    let associative1 = composePro(toUpperCase, composePro(exclaim, nameHead));
    console.log(associative1('hello')) // NEVERMORE HELLO AO AO AO!!!

模板字符串

实现JavaScript中的模板字符串
先上个测试用例

    let name = 'More'
    let sex = 'man'
    let extra = '!!!'
    let data = {name, sex, extra}

    // ${} 类型的模板字符串
    let strWith$ = '${extra} Hello ${name}, you are a ${sex}'
    console.log(render1(strWith$)(data))
    // !!! Hello More, you are a man

    // {{}} 类型的模板字符串
    let strWithDoubleParentheses = '{{extra}} Hello {{name}}, you are a {{sex}}'
    console.log(render2(strWithDoubleParentheses, data))
    // !!! Hello More, you are a man

主要使用到的技术是RegExp,和字符串中的replace方法,这里不再详细赘述RegExp MDNreplace

正则表达式

    let name = 'More' 
    let sex = 'man' 
    let extra = '!!!' 
    let data = {name, sex, extra}
    // ${} 类型的模板字符串 
    let strWith$ = '${extra} Hello ${name}, you are a ${sex}' ;
    console.log(render1(strWith$)(data))
    // !!! Hello More, you are a man 
    // {{}} 类型的模板字符串 
    let strWithDoubleParentheses = '{{extra}} Hello {{name}}, you are a {{sex}}'
    console.log(render2(strWithDoubleParentheses)(data)) 
    // !!! Hello More, you are a man

匹配${}的正则

    reg = /\$\{(\w+)\}/g
    //g全局匹配
    //特殊字符需要转译
    //()匹配的是\$\{和\w之间的值
    //补充知识:(\w)和(\w+),第一个正则是\w只匹配单词长度为一个字母的单词,
    function render1(str){
        return function(data){
            return str.replace(/\$\{(\w+)\}/g,(item,key)=>data[key])
        }
    }
    function render2(str){
        return function(data){
            return str.replace(/\{\{(.*?)\}\}/g,(item,key)=>data[key])
        }
    }

数组上的filter 使用方法

a = [1,2,3,4,5];
a.filter((item,index,arr)=>item==3)//[3];
console.log(a)//[1,2,3,4,5]

实现

    Array.prototype.myFilter = function(callback,thisArg){
    // callback回调函数,thisArg回调函数执行时使用的值
    if(!Array.isArray(this)){
        throw new TypeError('不是数组!');
    }
    if( typeof callback !='function'){
        throw new TypeError('第一个参数应为函数')
    }
    const result = [];
    for(let i = 0; i< this.length;i++){
        if(callback.call(thisArg,this[i],i,this)){
            result.push(this[i])
        }
    }
    return result;
}

//测试
let arr = [1, 2, 3, 4, 5, 6]
let test1 = arr.myFilter(item => item > 3)
console.log('test1: ', test1);