front-end-interview-questions icon indicating copy to clipboard operation
front-end-interview-questions copied to clipboard

【手写实现】JavaScrip头部高频手写实现面试题目汇总

Open yayxs opened this issue 4 years ago • 1 comments

手写instanceof 实现的原理

function myInstanceof(left,right){
    // 首先判断基本数据类型
    if(typeof left!=='object' || left === null) return false
    // getProtypeOf是Object对象自带的api 获取参数的原型对象
    let proto = Object.getPrototypeOf(left)
    while(true){
        if(proto===null)return false
        if(proto === right.prototype) return true // 找到相同的原型对象 返回true
        proto = Object.getPrototypeOf(proto)
    }
}

console.log(myInstanceof(123,Number)) // false

console.log(myInstanceof(new Number(123),Number)) // true

yayxs avatar Jul 02 '20 14:07 yayxs

  • 本文首次发布时间:2020年12月02日
  • 建议阅读时长:1H
  • 建议阅读对象:初中级前端工程师、JavaScript爱好者
  • 文章没有涉及Promise 涉及较少数组相关,后续会更新数组专题,以及异步编程专题,当然啦也有不少常见的你喜欢的手写实现没有出现,说不定哪天你回来看看就更新上去了呦

更新记录

  • 2020年12月08日 增加 form表单提交
  • 2020年12月26日 增加原生ajax的封装

前言

这是前端厚说 大系列第三篇正式的文章。作为前端开发者,那么html 我们抛砖引玉了一篇,那么css 我们也简单分享了一篇,自然而然要轮到Js 了,接下直接来看看“所谓的”手写实现。其实在整理学习的过程中,我发现了一个道理:就是为什么这些是老生常谈的问题,或者说话题。不难发现这些正是身为一个前端开发者最基本的东西,且不说各种的框架,像this作用域链 闭包 继承 等等其实在我们实际的开发中或者说那些框架中无处不在。告别焦虑且不贩卖焦虑 。另:案例代码在文末

手写实现ajax请求

 /* 封装ajax函数
     * @param {string}opt.type http连接的方式,包括POST和GET两种方式
     * @param {string}opt.url 发送请求的url
     * @param {boolean}opt.async 是否为异步请求,true为异步的,false为同步的
     * @param {object}opt.data 发送的参数,格式为对象类型
     * @param {function}opt.success ajax发送并接收成功调用的回调函数
     */
    ajax (opt) {
        opt = opt || {};
        opt.method = opt.method.toUpperCase() || 'POST';
        opt.url = opt.url || '';
        opt.async = opt.async || true;
        opt.data = opt.data || null;
        opt.success = opt.success || function () { };
        let xmlHttp = null;
        if (XMLHttpRequest) {
            xmlHttp = new XMLHttpRequest();
        } else {
            xmlHttp = new ActiveXObject('Microsoft.XMLHTTP');
        }
        this.XHR = xmlHttp;
        var params = [];
        for (var key in opt.data) {
            params.push(key + '=' + opt.data[key]);
        }
        var postData = params.join('&');
        if (opt.method.toUpperCase() === 'POST') {
            xmlHttp.open(opt.method, opt.url, opt.async);
            xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
            xmlHttp.send(postData);
        } else if (opt.method.toUpperCase() === 'GET') {
            xmlHttp.open(opt.method, opt.url + '?' + postData, opt.async);
            xmlHttp.send(null);
        }
        xmlHttp.onreadystatechange = function () {
            if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                opt.success(xmlHttp.responseText);
            }
        };
    }

手写实现form表单提交


function submit(){
  let xhr = new XMLHttpRequest()
  xhr.onreadystatechange = function(){
    if(xhr.readyState == 4){
      if(xhr.status>=200&&xhr.status<300 || xhr.status ==304){
        console.log(xhr.responseText)
      }
    }
  }
  xhr.open('post','https://jsonplaceholder.typicode.com/todos')
  xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded')
  let form = document.getElementById("user-info");
  xhr.send(serialize(form));
}

手写实现防抖函数(debounce/防抖动)

  • 第一个防抖函数
let num =1
      const container = document.querySelector('.container')
      // 基本函数内容
      function commonFnCon(){
        container.innerHTML = num++
      }
      function baseShowNumber(){
        commonFnCon()
        console.log(this)  // <div>num</div>
      }
      function firstShowNumber(){
        commonFnCon()
        console.log(this) // this 指的是 window
      }
      
      function firstDebounce(fn,wait){
        let timerId = null;
        return function (){
          if(timerId) clearTimeout(timerId)
          // 一句话:一件事情触发了,1s内不再触发此事件
          timerId = setTimeout(fn,wait)
        }
      }
      // container.onmousemove = baseShowNumber
      container.onmousemove = firstDebounce(firstShowNumber,1000)
  • 第二个防抖函数解决this的指向问题
let num =1
      const container = document.querySelector('.container')
      // 基本函数内容
      function commonFnCon(){
        container.innerHTML = num++
      }
      function baseShowNumber(){
        commonFnCon()
        console.log(this)  // <div>num</div>
      }
      function secShowNumber(){
        commonFnCon()
        console.log('sec',this) // this 指的是 window
      }
      
      function secDebounce(fn,wait){
        let timerId = null;
        return function (){
          let ctx = this 
          console.log('ctx',ctx) // 此时的ctx 就是baseShowNumber中的<div>num</div>

          if(timerId) clearTimeout(timerId)
          // 一句话:一件事情触发了,1s内不再触发此事件
          timerId = setTimeout(()=>{
            // 接下来就是把当前环境的this绑定到事件函数(这里指的是baseShowNumber)上
            // 并执行该事件函数
            fn.apply(ctx)
          },wait)
        }
      }
      // container.onmousemove = baseShowNumber
      container.onmousemove = secDebounce(secShowNumber,1000)

  • 第三个防抖函数修复事件对象为undefined的问题
let num =1
      const container = document.querySelector('.container')
      // 基本函数内容
      function commonFnCon(){
        container.innerHTML = num++
      }
      function baseShowNumber(e){
        commonFnCon()
        console.log(e) // MouseEvent 
        console.log(this)  // <div>num</div>
      }
      function thirdShowNumber(e){
        commonFnCon()
      }
      
      function thirdDebounce(fn,wait){
        let timerId = null;
        return function (){
          let ctx = this
          let args =  arguments
          console.log('ctx',ctx) // 此时的ctx 就是baseShowNumber中的<div>num</div>
          console.log('args',arguments) // 此时的arguments 刚好是个伪数组,其中包含事件对象
          if(timerId) clearTimeout(timerId)
          // 一句话:一件事情触发了,1s内不再触发此事件
          timerId = setTimeout(()=>{
            // 接下来就是把当前环境的this绑定到事件函数(这里指的是baseShowNumber)上
            // 并执行该事件函数
            fn.apply(ctx,args)
          },wait)
        }
      }
      // container.onmousemove = baseShowNumber
      container.onmousemove = thirdDebounce(thirdShowNumber,1000)
  • 小结

上述实现的依然是不够完整的,接下来自己再延伸探索吧,贴上我们企业项目中的防抖函数

const debounce = (fn, delay, isImmediate) => {
    var timer = null;
    return function() {
        var that = this;
        var args = [].slice.call(arguments);
        var callNow = !timer && isImmediate;

        if(timer) clearTimeout(timer);

        // 非立即执行
        timer = setTimeout(function() {
            timer = null;
            if(!isImmediate) fn.apply(that, args);
        }, delay);
        // 立即执行
        if(callNow) fn.apply(that, args);
    }
};
 export {
     debounce
 }
  • 业务场景
    • 主要是点击按钮刷新操作,用于防止频繁刷新
    • 还有就是form表单的验证(异步调接口的验证场景)

手写实现节流函数(throttle)

  • 认识节流

节流是每隔一段时间,只执行一次事件,防抖是一件事情触发了,1s内不再触发此事件

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

手写实现深浅拷贝(深浅克隆)

背景

深浅拷贝 是面试中的明星话题

首先明确一点我们接下来探讨的都是引用类型,一般我们拷贝的也就是像数组对象这种较为复杂的数据类型

什么是引用类型,我下边举个例子,也就是说 你和弟弟都是家里的人 一旦你弟弟 改变了你家的布局,那么你俩拿钥匙回到家看到的是一样的,都是改变后的样子 也就是说 你俩是一家人(这里就不用女朋友举例了)

如何切断数据,就是重新拷贝一份 浅拷贝就是表面的拷贝 深拷贝就是无限层级的拷贝下去

浅拷贝

先来说说浅拷贝,浅拷贝:创建一个新对象,有旧对象原始属性值(基本类型,拷贝的是基本类型;引用类型,便是内存地址)一份精确拷贝 其中一个对象地址改变,相互影响(也就是说虽然拷贝过了,但是还是会相互影响的)

let obj1 = {
  name: "张三",
  age: 18,
};
let obj2 = obj1;
obj2.name = "李四";

console.log(obj1.name); // 此时第一个对象的name属性就被改掉了

我们发现,我们通过一个简单的 赋值操作来完成这一操作 那我们是不是也可以通过一个简单的函数来实现,那就是

const shallow = (target) => {
  let obj = {};
  for (let prop in target) {
    obj[prop] = target[prop];
  }
};

从数组来看的话

let targetArr = [{name:'oldName'},"1", "1", 1, 1, true, true, undefined, undefined, null, null,]

let resultArr = targetArr.concat()
resultArr[0]['name'] = 'newName'

console.log('old',targetArr) // 此时 targetArr 的第一个元素的 name 也被修改了
console.log('new',resultArr)

具体实现


function firstShallowClone(target){
    if(typeof target !== 'object') return
    let result = Array.isArray(target) ? [] :{}
    for(let k in target){
        if(target.hasOwnProperty(k)){
            result[k] = target[k]
        }
    }
    return result
}

深拷贝

深拷贝的核心思路便是 拷贝加递归 也就是说当对象的某一个属性还是个对象的时候,我们需要对之进一步拷贝,从内存完整拷贝,在堆中重新开启区间,对象地址改变不会影响

第一个深拷贝:通过JSON的两个API

  • JSON.parse() 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象
  • JSON.stringify() 方法将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串

实现 JavaScript 中的深拷贝,有一种非常取巧的方式 —— JSON.stringify,

let obj = {
  name: "yayxs",
  fav: [
    {
      type: "play"
    }
  ],
  friend: {
    name: "wanghuahua"
  }
};

const objStr = JSON.stringify(obj) // deepClone.js:16 {"name":"yayxs","fav":[{"type":"play"}],"friend":{"name":"wanghuahua"}}
const objCopy = JSON.parse(objStr)



objCopy.fav.splice(0,1)

console.log(obj['fav']) // [{}]
console.log(objCopy['fav']) //[]

但是如果单个元素是函数的话,我们来试一下


let fnArr = [

    ()=>{
        console.log(1)
    },
    ()=>{
        console.log(2)
    }
]

console.log(JSON.parse(JSON.stringify(fnArr))); // [null, null]

第二个深拷贝:递归拷贝

判断一下属性值的类型,当目前属性值的类型是个对象的时候,然后递归克隆,

function firstDeepClone(target){
  //  如果是 值类型 或 null,则直接return
  if(typeof target !== 'object' ||target===null ) {
    return target
  }
  // 结果对象
  let res = target instanceof Array ? [] : {};
  for(let key in res){
    if(obj.hasOwnProperty(key)){
      // 首先判断当前key 所对应的属性值是否是个引用类型
      if(typeof obj[key] === 'object'){
        res[key] = firstDeepClone(obj[key])
      }else{
        res[key] = obj[key]
      }
    }
  }
}

手写实现 JavaScript 中的 New 操作符

延伸的面试题

根据new操作符相关的知识点一般会 延伸出以下的面试题 ,面试官你是否有很多问号

  • 问题一:new 之后都做了些什么??
  • 问题二:能否手写 new 操作符原理??
  • 问题三:通过 new 的方式创建对象和通过字面量创建有什么区别

mdn 关于 new 运算符关键字的描述

  1. 创建一个空的简单 JavaScript 对象(即{});
  2. 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  3. 将步骤 1 新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

以上 4 条是MDN 上关于 new 操作符(或者说关键字)的面试,简单的来体验下利用构造函数来new 一个对象

function Person(name, age) {
  console.log("this", this);
  this.name = name;
  this.age = age;
}
// 然后在**构造函数添加原型方法**
Person.prototype.height = 180;
Person.prototype.sayName = function() {
  console.log(this.name);
};
let p = new Person("yayxs", 20);
console.log(p.name); // yayxs
console.log(p.age);
20;
p.sayName(); // yayxs
console.log(p.__proto__ === Person.prototype); // 对象p(实例)的原型属性指向构造函数的原型,

既然我们通过自定义,其使用的方式大体跟new 是一样的。

// ------ 使用new的时候

const p = myNew Person('yayxs',20) // 其返回的结果是一个对象

// ---------

第一版的 myNew

大体思路是声明一个对象,取出当前的构造函数,以及参数,让新对象的原型属性指向构造函数的原型,然后调用构造函数,传入对象的参数

function myNew() {
  let obj = new Object(),
    [constructor, ...args] = [...arguments];
  obj.__proto__ = constructor.prototype;

  constructor.apply(obj, args);
  return obj;
}

第二版的myNew

经过上文的简单案例我们可以得知,

  • new 一个构造函数得到一个对象,它的原型属性(也就是** proto **)与该构造函数的原型是全等

  • new 通过构造函数 Persion 创建出来的实例可以访问到构造函数中的属性,就像这样

    console.log(xiaoMing.name); // 小明
    
  • 言简意赅:new 出来的实例对象通过原型链和构造函数联系起来

构造函数说白了也是一个函数,那是函数就可以有返回值

function Person(name) {
  this.name = name;
  //   return 1; // 返回内部新创建的对象
  //   return "1"; // 返回内部新创建的对象
  // return null; // 返回内部新创建的对象
  //   return undefined; // 返回内部新创建的对象
  //   return {}; // {} // 直接返回
  return function() {}; // 直接返回
  return [1]; // [1] // 直接返回
}
let p = new Person("李四");
console.log(p);

有了给构造函数返回一个值得想法,那就通过不同的数据类型 进行测试得出结论

  • 不同的数据类型返回的效果是不一样的,像数字 1 字符串”1“ ,返回的依然是内部创建的对象
  • 那如果返回一个对象({})或者说数组([]) 都会直接返回回去

小结

也就是说,构造函数一般不需要return

  • 返回一般的数据类型吧,不起作用
  • 返回对象吧, new 的意义又何在呢

function myNew(){
  let obj = new Object(),
  [constructor,...args] =  [...arguments]
   obj.__proto__ = constructor.prototype;

  let res =  constructor.apply(obj,args)
  return =  typeof res === 'object' ? res : obj;
}

手写一个自己的 myNew 小结

如果自己实现一个 new 的话,首先要满足它的几点效果

  1. 一个构造函数会返回一个对象,那函数里就应该有对象

    let obj = {};
    
  2. 并将其__proto__属性指向构造函数的prototype属性

    obj.__proto__ = constructor.prototype;
    
  3. 调用构造函数,绑定 this

    constructor.apply(obj, args);
    
  4. 返回原始值需要忽略,返回对象需要正常处理

    res instanceof Object ? res : obj;
    

箭头函数使用new

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

手写实现 call 与 apply

前言

函数可以被传递、用作对象等让我们先看一段代码,函数虽然将调用传递给原始的方法,但是上下文this 却会存在不见的地方,这也就引出我们的call()方法,call 是什么黑魔法,不过是一个内置函数方法,使用的方式是 add.call(),并且add函数自动执行输出结果 3

// 参数context 指定的this值
// arg1 参数一
// arg2 参数二
func.call(context, arg1, arg2, ...)
function add(a, b) {
  console.log("add函数中this", this);
  console.log(a + b);
}
add(1, 2); // this指向window
add.call({ name: "yayxs" }, 1, 2); // this指向传入的 {name:'yayxs'}

实现第一版 call

这时候我们把传入的对象抽离出来

let o = {
  name: "yayxs",
};

function sayName() {
  console.log(this.name);
}

sayName.call(o); // yayxs

Function.prototype.myCall = function(ctx) {
    console.log(this) // 其中this 就是sayName 这个函数
    console.log(ctx) //  {name: "yayxs"}
    ctx.tempFunc = this
    ctx.tempFunc()
    delete ctx.tempFunc
};

sayName.myCall(o,'参数一','参数二') // 理论上输出 yayxs

实现第二版 call

我们上述的myCall 传入的参数一和参数二并没有参与感,再完善一下

Function.prototype.myCall = function(ctx) {
    console.log(this) // 其中this 就是sayName 这个函数
    console.log(ctx) //  {name: "yayxs"}
    console.log('arguments',arguments)
    let tempArgs = [];// 用来存放参数
    for(let i=1,len=arguments.length;i<len;i++){
        console.log(arguments[i]) // 第一遍循环体 输出参数一 第二遍循环体 参数二
        tempArgs.push('arguments[' + i + ']');
    }
    console.log(tempArgs);
    ctx.tempFunc = this
    // ctx.tempFunc()
    let evalScript = 'ctx.tempFunc(' + tempArgs +')'
    eval(evalScript);
    delete ctx.tempFunc
};

手动实现 apply


// ---------------- 实现myApply
Function.prototype.myApply = function(ctx,arr){
  ctx.tempFunc = this
  let result
  if(!arr){
    result = ctx.tempFunc() // 直接执行
  }else{
    let args = []
    for (let i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']');
  }
    result = eval('ctx.tempFunc(' + args + ')')
  }
  delete ctx.tempFunc
  return result 
}

总结

总体来说,call apply 函数的作用都是用来改变this的指向。目前的 js 还存在回调函数这一现象,尤其在框架中一些异步回调也是十分的常见,难免this会迷失方向既有不同也有相似,倘若自己手写代码实现callapply

  1. 获取被绑定的函数
  2. 被绑定的函数追加到劫持替换的对象
  3. 被绑定的函数追加到劫持替换的对象
方法名 作用 是否自动执行 参数列表
call 改变 this 指向 自动执行函数 一般列表
apply 改变 this 指向 自动执行函数 数组形式

手写实现bind 函数

在手写实现bind之前,我们先来回忆一下bind 的使用场景,我们就说在react 框架中好了,

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      num: 0,
    };
  }
  // 1 合成事件中的setState
  handleClick() {
    console.log(this.state);

    this.setState({ num: this.state.num + 1 }); // 合成事件执行完,  state 并没有更新 造成所谓的 异步 try 代码块执行完事之后
    console.log(this.state);
  }
  componentDidUpdate() {
    console.log(this.state.num);
  }
  render() {
    return (
      <>
        {this.state.num}
        // 可以看到我们使用 bind函数来绑定this
        <button onClick={this.handleClick.bind(this)}>按钮</button>
      </>
    );
  }
}

export default App;

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函 数的参数,供调用时使用。但是我们需要事先说一下bind 是需要Polyfill的。因为大部分的浏览器都实现了内置的Function.prototype.bind 的实现,也有些是不支持的

const user = {
    name:'yayxs',

}

function showYourself(){
    console.log(this.name)
}

const result = showYourself.bind(user)

第一个bind

const user = {
  name: "yayxs",
};

function showYourself(sex) {
  console.log(this.name);
  console.log(sex)
}

let resultFn;
resultFn = showYourself.bind(user);

// console.log(resultFn)

Function.prototype.myFirstBind = function(ctx) {
  console.log(ctx); // user {name:'yayxs'}
  let _this = this;

  // 第一步返回一个函数
  return function() {
    //    _this 此时是showYourself函数
    return _this.apply(ctx);
  };
};

resultFn = showYourself.myFirstBind(user);

// console.log(resultFn)

第二个bind

Function.prototype.secBind = function() {
  let self = this, // 保存原来的函数
    context = [].shift.call(arguments); // 需要绑定的this的上下文
  args = [].slice.call(arguments); // 剩余的参数转成数组
  return function() {
    // 返回一个新的函数
    // 执行新的函数的时候,会把之前传入的context当做新函数体内的this 并且组合两次分别差UN额逇参数 作为新函数的参数
    return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
  };
};

手写实现 String.prototpye.trim() 方法

ECMAScript 在所有字符串上都提供了 trim()方法。这个方法会创建字符串的一个副本,删除前、后所有空格符,再返回结果。 由于 trim()返回的是字符串的副本,因此原始字符串不受影响,即原本的前、后空格符都会保留。

为简化子字符串替换操作,ECMAScript 提供了 replace()方法。这个方法接收两个参数,第一个参数可以是一个 RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记,

用 replace 结合正则实现清除字符串两边空格的方法 保留两侧的空格,而清除内部的空格

String.prototype.trim
if (!String.prototype.trim) {
    String.prototype.trim = function () {
        return this.replace(/^\s+|\s+$/gm, '');
    }
}

手写实现EventBus/EventEmitter

// eventBus.js 文件
import Vue from "vue";
const EventBus = new Vue(); // 本质上也是 Vue 实例
export default EventBus;
// main.js
import EventBus from "eventBus.js";

Vue.prototype.EventBus = EventBus;
// 派发事件
this.$EventBus.$emit("sendVal", "派发事件的");
// 监听事件
this.$EventBus.$on("sendVal", (val) => {
  console.log(val);
});
class EventEmitter {
  constructor() {
    this.handles = new Map(); // 存储实践回调之间的关系
  }

  on(evtName, cb) {
    if (!this.handles.has(evtName)) {
      this.handles.set(evtName, []);
    }

    this.handles[evtName].push(cb);
  }

  emit(evtName, ...args) {
    if (this.handles.has(evtName)) {
      for (let i = 0, len = this.handles[evtName].length; i < len; i++) {
        this.handles[evtName][cb](...args);
      }
    }
  }

  off(evtName, cb) {
    const cbs = this.handles[evtName];
    const idx = cbs.indexOf(cb);
    if (idx !== -1) {
      cbs.splice(idx, 1);
    }
  }
  once(evtName, cb) {
    const warp = (...args) => {
      cb(...args);
      this.off(evtName, warp);
    };
    this.on(evtName, warp);
  }
}

手写实现数组去重的方法

要想搞明白数组去重的各种方案,第一步要做的事就是什么是重复的元素,先来看一段代码结果

/**
 * 谈到数组去重,几乎是面试必备的一道开胃菜
 * 要想数组去重,第一件事就是有一个数组
 */
// 首先第一步
console.log(1 === 1); // true
console.log("1" === "1"); // true
console.log("true" === "true"); // true
console.log(false === false); // true
console.log(undefined === undefined); // true
console.log(null === null); // true

console.log(NaN === NaN); // false
console.log({} === {}); // false
console.log([] === []); // false

接着第二步:准备含有重复元素的目标数组

let targetArr = ["1", "1", 1, 1, true, true, undefined, undefined, null, null];

console.log(targetArr);

第三步:写去重方法,这时候才是正是写方法的时候

/**
 * desc 第一种方案 双层for循环
 */
function unique1(arr) {
  let result = []; // 结果数组
  for (let i = 0, len = arr.length; i < len; i++) {
    for (var j = 0, resLen = result.length; j < resLen; j++) {
      if (arr[i] === result[j]) {
        break;
      }
    }

    if (j === result.length) {
      result.push(arr[i]);
    }
  }
  return result;
}
/**
 * desc 第二种方案 indexOf
 */

const unique2 = (arr) => {
  let result = [];
  for (let i = 0, len = arr.length; i < len; i++) {
    if (result.indexOf(arr[i]) === -1) {
      // 在结果数组中没有找到元素
      result.push(arr[i]);
    }
  }
  return result;
};
/**
 * desc 第三种方案 先给数组排序结合 sort函数
 */

const unique3 = (target) => {
  target.sort();
  let result = [target[0]]; // 取出第一个元素

  for (let i = 1; i < target.length; i++) {
    target[i] !== target[i - 1] && result.push(target[i]); // 当前项和它的前一项不同的时候,才添加进结果数组
  }
  return result;
};
/**
 * desc 第四种方案 使用filter 函数结合 indexof
 */

const unique4 = (target) => {
  return target.filter((item, index) => {
    return target.indexOf(item) === index; // 数组的下标与检索的下标一致
  });
};
/**
 * desc 第五种方案 使用对象的键值对 结合obj.hasOwnProperty()
 */

const unique5 = (target) => {
  let obj = {}; // 初始化一个空的对象
  let result = new Array();
  result = target.filter((item, index) =>
    // typeof item + item 主要是考虑到 对象的key 数值1 会被搞成 '1'
    obj.hasOwnProperty(typeof item + item)
      ? false
      : (obj[typeof item + item] = true)
  );
  return result;
};
/**
 * desc 第六种方案 使用ES6的新语法Set
 *
 */
const unique6 = (target) => {
  // Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
  let x = new Set(target); // Set { '1', 1, true, null }

  return [...x]; // 转为数组
};
/**
 * desc 第七种方案 使用ES6的Array.from
 *
 */
const unique7 = (target) => {
  // Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
  let x = new Set(target); // Set { '1', 1, true, null }

  return Array.from(x); // 转为数组
};
/**
 * desc 第八种方案 使用哈希表
 *
 */

const unique8 = (target) => {
  let result = []; // 结果数组
  let hash = {};
  for (let ele of target) {
    if (!hash[ele]) {
      // hash 中没有数组中的元素
      result.push(ele); // 把元素放进去
      hash[ele] = true; // hash中已经存在了标记为true 下次循环就不会接着往结果数组中放相同的元素了
    }
  }
  return result;
};
/**
 * desc 第九种方案 使用Map 类型
 * 缺点 hash中存在相同的 key 就不往下找了, 但 '1' 和 1 是不同的元素
 * 请同学自行测试吧哈哈~
 */

const unique9 = (target) => {
  let map = new Map(); // 初始化 map
  let result = new Array(); // 初始化 数组
  for (let i = 0; i < target.length; i++) {
    if (map.has(target[i])) {
      map.set(target[i], true);
    } else {
      map.set(target[i], false);
      result.push(target[i]);
    }
  }
  return result;
};
/**
 * desc 第十种方案 双层for循环变体
 *
 *
 */

const unique10 = (target) => {
  //   let result = [];

  for (let i = 0; i < target.length; i++) {
    for (let j = i + 1; j < target.length; j++) {
      if (target[i] === target[j]) {
        // 如果两项元素相同的话,则从目标元素中删除一个
        target.splice(j, 1);
        // splice 会改变原数组,所以相关的长度都要减去一
        i--;
        j--;
      }
    }
  }

  return target;
};
/**
 * desc 第十一种方案 数据的遍历结合includes
 *
 *
 */

const unique11 = (target) => {
  let result = [];
  for (let ele of target) {
    // 其中 ele 是每一目标元素中每一元素
    !result.includes(ele) && result.push(ele); // 如果结果数组中没有ele就添加进去
  }
  return result;
};
/**
 * desc 第十二种方案 数据reduce
 *
 *
 */

const unique12 = (target) => {
  return target.reduce((previousValue, currentValue, currentIndex) => {
    return previousValue.includes(currentValue)
      ? previousValue
      : [...previousValue, currentValue];
  }, []);
};
console.log(unique1(targetArr)); // ["1", 1, true, undefined, null]
console.log(unique2(targetArr)); // ["1", 1, true, undefined, null]
console.log(unique3(targetArr)); // ["1", 1, null, true, undefined]
console.log(unique4(targetArr)); // ["1", 1, null, true, undefined]
console.log(unique5(targetArr)); // ["1", 1, null, true, undefined]

手写实现输出由*号组成的三角形


 /**
       *      *       1----1
       *      **      2----2
       *      ***     3----3
       *      ****    4----4
       */
      for (let row = 0; row < 4; row++) { // 行数
        for (let num = 0; num < row; num++) {
          document.write("*");
        }
        document.write("<br />");
      }

      for(let row = 4;row>0;row--){
          for(let num=0;num<row;num++){
            document.write("*");
          }
          document.write("<br />");
      }
  • 作者:洋小洋同学
  • 上文所及代码案例可在 ……/demos/written
  • 备用浏览地址 ……debounce.html
  • 参考
    • 冴羽的博客
    • 《JavaScript设计模式与开发实践》
  • 如果对你有帮助的话多谢点赞评论,后续希望分享什么专题?

yayxs avatar Dec 26 '20 06:12 yayxs