「前端厚说」JavaScrip头部高频手写实现面试题目汇总

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

更新记录

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

前言

这是前端厚说 大系列第三篇正式的文章。作为前端开发者,那么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设计模式与开发实践》
  • 如果对你有帮助的话多谢点赞评论,后续希望分享什么专题?