面试第三篇

  

数据结构

常见数据结构

八种常见数据结构:数组、栈、队列、链表、图、树、前缀树、哈希表

数组与链表

数组(Array)是最简单,也是最常用的数据结构。栈和队列都是由数组衍生出来的。

数组根据维度区分分为一维数组和多维数组

链表(Linked List)也是线性结构,它与数组看起来非常像,但是它们的内存分配方式、内部结构和插入删除操作方式都不一样。

链表是一系列节点组成的链,每一个节点保存了数据以及指向下一个节点的指针。链表头指针指向第一个节点,如果链表为空,则头指针为空或者为null。

链表可以用来实现文件系统、哈希表和邻接表。

链表分为单向链表和双向链表。双链表每个节点除了保存数据及指向下一个节点的指针,还保存了前一个节点的指针。

还有一类链表是环形链表,链表的尾部节点指向链表中某个节点

栈与队列

栈中的元素采用LIFO (Last In First Out),即后进先出

队列(Queue)与栈类似,都是采用线性结构存储数据。它们的区别在于,栈采用LIFO方式,而队列采用先进先出,即FIFO(First in First Out)

图(graph)由多个节点(vertex)构成,节点之间可以互相连接组成一个网络。(x, y)表示一条边(edge),它表示节点x与y相连。边可能会有权值(weight/cost)

树(Tree)是一个分层的数据结构,由节点和连接节点的边组成。树是一种特殊的图,它与图最大的区别是没有循环。

树被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。

树的分类

N叉树、平衡树、二叉树、二叉查找树、平衡二叉树、红黑树、2-3树

二叉树又分为满二叉树、完全二叉树、平衡二叉树

二叉树和二叉查找树是最常用的树。

霍夫曼树、B树、B+树、B*树

B树

B+树

特点:

  1. 关键字数和子树相同。在 B+ 树中,节点的关键字代表子树的最大值,因此关键字数等于子树数。
  2. 非叶子节点仅用作索引,它的关键字和子节点有重复元素。除叶子节点外的所有节点的关键字,都在它的下一级子树中同样存在,最后所有数据都存储在叶子节点中。
  3. 叶子节点用指针连在一起。叶子节点包含了全部的数据,并且按顺序排列,B+ 树使用一个链表将它们排列起来,这样在查询时效率更快。

遍历方法

前缀树

前缀树(Prefix Trees或者Trie)与树类似,用于处理字符串相关的问题时非常高效。它可以实现快速检索,常用于字典中的单词查询,搜索引擎的自动补全甚至IP路由。

哈希(Hash)将某个对象变换为唯一标识符,该标识符通常用一个短的随机字母和数字组成的字符串来代表。哈希可以用来实现各种数据结构,其中最常用的就是哈希表(hash table)与hashmap。

哈希表通常由数组实现,哈希表的性能取决于三个指标:

哈希函数、哈希表的大小、哈希冲突处理方式

数组和链表的区别

线性表包含顺序线性表和链表。顺序表是用数组实现的,链表是用指针实现的,在内存中不连续。

数组是顺序存储结构,数组的内存要求连续的区域,因此要求连续的内存空间,查找速度快,可以随机访问和修改,插入删除效率低,,大小固定,不能动态扩展

链表是链式存储结构,链表的内存可以存在任何地方,不要求连续,插入删除速度快,不能随机查找,必须从第一个开始遍历,效率低,且不允许随意存取,大小没有固定,扩展灵活

堆和栈(数据结构与内存)

数据结构中的堆和栈

堆是常用的树形结构,是一种特殊的完全二叉树。只有所有节点的值总是不大于或者不小于其父节点的值的完全二叉树被称为堆。根节点最大的称为大顶堆,根节点最小的称为小顶堆,

堆用数组存储,

内存中的堆和栈

一个c++程序内存分配

全局区(static):存放全局变量和静态变量。初始化的在一块,未初始化的在一块

栈区(stack):存放局部变量、函数参数值等,由编译器自动分配释放,操作方式类似于数据结构中的栈

堆区(heap):由程序员分配释放,若程序员不释放程序结束时由系统回收。分配方式类似于数据结构中的链表,与栈不同

文字常量区:常量和字符串放在这里,程序结束后由系统释放。

程序代码区:存放函数体,为二进制代码

栈由系统分配释放,速度快,空间小,windows下不超过2m,堆由程序员自己生成,速度慢,空间大且灵活,

0.1+0.2 != 0.3

0.10.2的二进制都是以1100无限循环的小数,

0.1的二进制

0.0001100110011001100110011001100110011001100110011001101

0.2的二进制

0.001100110011001100110011001100110011001100110011001101

理论上讲相加应该得到

0.0100110011001100110011001100110011001100110011001100111

实际上JavaScript相加得到

0.0100110011001100110011001100110011001100110011001101

JavaScript中使用的是64位双精度浮点数编码,所以它的符号位1位,指数位占11位,尾数位占52位。

尾数位就是存储科学计数法后的有效数字;

由于限制,有效数字第53位及以后的数字是不能存储的,它遵循,如果是1就向前一位进1,如果是0就舍弃的原则。

0.1的二进制科学计数法第53位是1,0.2有着同样的问题,其实正是由于这样的存储,在这里有了精度丢失,导致了0.1+0.2!=0.3

BigInt 是第七种原始类型。

BigInt 是一个任意精度的整数。这意味着变量现在可以计算9007199254740991即最大安全整数以上的数字。

const b = 1n;  // 追加 n 以创建 BigInt

typeof 10;
⇨ 'number'
typeof 10n;
⇨ 'bigint'

解决方法

1.把计算数字 提升 10 的N次方 倍 再 除以 10的N次方。N>1.

(0.1*1000+0.2*1000)/1000==0.3
//true

2.使用Number.EPSILON方法

Number.EPSILON=(function(){   //解决兼容性问题
        return Number.EPSILON?Number.EPSILON:Math.pow(2,-52);
      })();
//上面是一个自调用函数,当JS文件刚加载到内存中,就会去判断并返回一个结果,相比if(!Number.EPSILON){
  //   Number.EPSILON=Math.pow(2,-52);
  //}这种代码更节约性能,也更美观。
function numbersequal(a,b){ 
    return Math.abs(a-b)<Number.EPSILON;
  }
//接下来再判断   
var a=0.1+0.2, b=0.3;
console.log(numbersequal(a,b)); //这里就为true了

排序算法

冒泡排序、快速排序、选择排序、插入排序、堆排序

冒泡排序: 最小的元素会慢慢浮到队列的顶端

从左到右逐一比较相邻的元素,如果不符合顺序交换位置,逐渐把大的放在最后,小的放在前面

选择排序

遍历,每次找到最小的放在最前面,依次

插入排序:

从第一个依次每次向后取一个,逐个检查插入准确位置

快速排序:

以左边第一个为基准,将大于的放在右边,小于的放在左边,依次排列

https://blog.csdn.net/java_green_hand0909/article/details/84970372

快速排序

const quickSort = function (arr) {
  // 递归结束条件
  if(arr.length < 2) return arr;

  // 基准
  const pivot = arr.splice(0, 1);
  // 左区
  const left = [];
  // 右区
  const right = [];

  // 将剩余元素按照一定规则,分配到左区、右区。
  for(let i = 0; i < arr.length; i++) {
    // 大于基准值的分配到右区,小于基准值的分配到左区
    if(arr[i] > pivot[0]) {
      right.push(arr[i])
    } else {
      left.push(arr[i])
    }
  }

  // 返回 左区 拼 基准 拼 右区, 再对左区、右区分别重选基准分区
  return quickSort(left).concat(pivot).concat(quickSort(right));
}

冒泡排序

var arr = [3, 4, 1, 2];
function bubbleSort (arr) {
  var max = arr.length - 1;
  for (var j = 0; j < max; j++) {
    // 声明一个变量,作为标志位
    var done = true;
    for (var i = 0; i < max - j; i++) {
      if (arr[i] > arr[i + 1]) {
        var temp = arr[i];
        arr[i] = arr[i + 1];
        arr[i + 1] = temp;
        done = false;
      }
    }
    if (done) {
      break;
    }
  }
  return arr;
}
bubbleSort(arr);

https://juejin.cn/post/6933377315573497864

前端

js

JavaScript为什么是单线程

1.消耗资源和成本:

如果 Javascript 被设计为多线程的程序,那么操作 DOM 必然会涉及到资源的竞争,那么这么语言必然会被实现的非常臃肿,那么在 Client 端中跑这么一门语言的程序,资源消耗和性能都将是不乐观的,同时在 Client 实现多线程还真没有那么刚性的需求;但是如果设计成单线程,并辅以完善的异步队列来实现,那么运行成本就会比多线程的设计要小很多了。

2.操作逻辑

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

js与ts的区别

ts比js更严谨,提供类型检查,是js的超集,ts执行时编译为js

ts报错也可以执行,可以编译成js

具体:

ts给js中添加了类、模块,可以把声明、数据、函数和类封装在模块中,类提供了接口

在编码期间可以检查错误,更高效

js设计模式

策略模式

​ 定义一系列算法,把它们一个个封装起来,并使它们可以相互替换

​ 意义:

​ 利用组合、委托等技术和思想,有效避免很多if条件语句

​ 提供了开放封闭原则,使代码更容易理解

​ 代码复用

单例模式

​ 保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例是否 存在,如果存在则直接返回,

代理模式

​ 为对象提供一个代用品或者占位符,以便控制它的访问

​ 意义:

​ 单一职责,只有代理对象才能引起该对象的变化

用代理来过滤掉一些不合规的访问,减少该对象的访问,可以提高性能。

观察者模式

定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知

意义:

支持简单的广播通信,自动通知所有订阅过的对象

装饰者模式

在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。

意义:原有方法维持不变,在原有方法上再挂载其他方法满足现有需求

​ 拓展对象功能

什么是DOM,怎么操作DOM,交换Dom

放进数组,交换数组元素

<div id="divs">
  <div class="a">
    1
  </div>
  <div class="b">
    2
  </div>
  <div class="c">
    3
  </div>
</div>
<script>
  $(document).ready(function(){
    var arr = $('divs').find('div').toArray();//把三个div放进数组
    var temp;
    tepm = arr[0];
    arr[0] = arr[2];
    arr[2] = temp;
    $('#divs').html(arr);
  })
</script>

new实例的过程、手写new的过程

版本1:

1.以构造器的prototype属性为原型,创建新对象;

2.通过 this 将属性和方法添加至这个对象2

3.最后返回 this 指向的新对象,也就是实例

版本2:

1.创建一个空对象,作为将要返回的对象实例,

2.将这个空对象的原型指向构造函数的prototype属性

3.将这个空对象赋值给函数内部的this关键字

4.开始执行构造函数内部代码

//接受一个函数
//最后返回一个对象
function new (fn) {
    return function () {
      var obj = { 
         '__proto__': fn.prototype
      }
      fn.apply(obj, arguments)      
      return obj
    }
}

访问对象属性加1

例如你有构造了一个对象 user // { age: 10 },

那么 user.age // 11 user.age // 12 … 如何构造一个这样的对象,让它的属性值每次被访问时就 + 1

通过Object.defineProperty重写get方法。

let user = {age:10}
 
Object.defineProperty(
  user.prototype,
  'age',
  {
    get:function() {
      return age++;
    }
  }
);
//Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。

手写getElementById

function getElementById(node, id){
    if(!node) return null;
    if(node.id === id) return node;
    for(var i = 0; i < node.childNodes.length; i++){
        var found = getElementById(node.childNodes[i], id);
        if(found) return found;
    }
    return null;
}
getElementById(document, "d-cal");

this、手写bind、call、apply

this 永远指向最后调用它的那个对象

改变this指向的方法:

  • 使用 ES6 的箭头函数
  • 在函数内部使用 _this = this
  • 使用 applycallbind
  • new 实例化一个对象

apply 和 call 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组,bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

手写bind、call、apply

bind

//调用原生案例
// Function.prototype.bind()样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 只变更fun中的this指向,返回新function对象
const newFun = fun.bind(_this)
newFun(1, 2)
//手写实现
Function.prototype.ownBind = function(context) {
  context = (typeof context === 'object' ? context : window)
  return (...args)=>{
    this.call(context, ...args)
  }
}
//调用手写案例 验证样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 只变更fun中的this指向,返回新function对象
const newFun = fun.ownBind(_this)
newFun(1, 2)

call

//调用原生案例
  function fun(arg1, arg2) {
    console.log(this.name)
    console.log(arg1 + arg2)
  }
  const _this = { name: 'YIYING' }
  // 接受的是一个参数列表;方法立即执行
  fun.call(_this, 1, 2)
//手写实现
  Function.prototype.ownCall = function(context, ...args) {
    context = (typeof context === 'object' ? context : window)
    // 防止覆盖掉原有属性
    const key = Symbol()
    // 这里的this为需要执行的方法
    context[key] = this
    // 方法执行
    const result = context[key](...args)
    delete context[key]
    return result
  }
//调用手写案例 验证样例
  function fun(arg1, arg2) {
    console.log(this.name)
    console.log(arg1 + arg2)
  }
  const _this = { name: 'YIYING' }
  // 接受的是一个参数列表;方法立即执行
  fun.ownCall(_this, 1, 2)

apply

//调用原生案例
  function fun(arg1, arg2) {
    console.log(this.name)
    console.log(arg1 + arg2)
  }
  const _this = { name: 'YIYING' }
  // 参数为数组;方法立即执行
  fun.apply(_this, [1, 2])
//手写
  Function.prototype.ownApply = function(context, args) {
    context = (typeof context === 'object' ? context : window)
    // 防止覆盖掉原有属性
    const key = Symbol()
    // 这里的this为需要执行的方法
    context[key] = this
    // 方法执行
    const result = context[key](...args)
    delete context[key]
    return result
  }
//调用手写案例,验证样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 参数为数组;方法立即执行
fun.ownApply(_this, [1, 2])

手写深拷贝与浅拷贝

区别:

浅拷贝是指拷贝内存地址,公用同一个堆内存,两个变量相互受影响,深拷贝是指,开辟一块内存空间,保存相同的值。互不受影响。

浅拷贝实现

三种方式

  • ES6:object.assign()
  • 展开运算符…
  • 自己封装函数实现

深拷贝实现

三种方式

  • JSON.parse() (但是如果里面有 function 和 undefined 不可用)
  • lodash
  • 自己封装

JSON.parse(JSON.stringify(X)) 的缺点:

在序列化JavaScript对象时,所有函数和原型成员会被有意忽略。

如:如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式

如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;

如果obj里有函数,undefined,则序列化的结果会把函数变成 undefined丢失,同时抛出错误;

如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null

JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;

通俗点说,JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。

手写深拷贝

function deepCopy(obj){
    //判断是否是简单数据类型,
    if(typeof obj == "object"){
        //复杂数据类型
        var result = obj.constructor == Array ? [] : {};
        for(let i in obj){
            result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
        }
    }else {
        //简单数据类型 直接 == 赋值
        var result = obj;
    }
    return result;
}

避免对象的循环引用

需要拷贝的源对象存在循环引用,所以deepClone 函数会不断入栈,最后栈溢出。代码运行后直接报错:Maximum call stack size exceeded,调用栈溢出

解决这一循环引用的问题其实很简单,我们只需要在每次对复杂数据类型进行深拷贝前保存其值,如果下次又出现了该值,就不再进行拷贝,直接截止。

选用 ES6 中的 WeakMap 或者 Map 数据结构来存储每一次的复杂类型的值,我们也要对原来的 deepClone 函数内部的逻辑封装到内部的另外一个函数内,目的是为了在内部函数外部我们定义映射,形成闭包

const deepClone = (obj) => {
   // 定义一个映射,初始化的时候将 obj 本身加入映射中
   const map = new WeakMap()
   map.set(obj, true)
   // 封装原来的递归逻辑
   const copy = (obj) => {
      if (!obj || typeof obj !== 'object') {
         return obj
      }
      const newObj = Array.isArray(obj) ? [] : {}
      for (const key in obj) {
         const value = obj[key]
         // 如果拷贝的是简单类型的值直接进行赋值
         if (typeof value !== 'object') {
            newObj[key] = value
         } else {
         	// 如果拷贝的是复杂数据类型第一次拷贝后存入 map
            // 第二次再次遇到该值时直接赋值为 null,结束递归
            if (map.has(value)) {
               newObj[key] = null
            } else {
               map.set(value, true)
               newObj[key] = copy(value)
            }
         }
      }
      return newObj
   }
   return copy(obj)
}

手写typeof与instanceof

typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断number, string, object, boolean, function, undefined, symbol 这七种类型,typeof可以判断出基本数据类型(当然除了null的数据类型为object的bug),还可以正确判断出某个对象是否为function,其余的Date,Array等无法判断

手写typeof

//使用Object.prototype.toString实现
function myTypeof(params){
  const type = Object.prototype.toString.call(params).slice(8, -1).toLowerCase()
  const map = {
    'number': true,
    'string': true,
    'boolean': true,
    'undefined': true,
    'bigint': true,
    'symbol': true,
    'function': true
  }
  return map[type] ? type : 'object'
}

instanceof 主要的作用就是判断一个实例是否属于某种类型,instanceof 也可以判断一个实例是否是其父类型或者祖先类型的实例。

instanceof相反,可以准确判断出复杂数据类型,但是无法判断简单数据类型.

手写instanceof

function instanceOf(left,right) {
    let proto = left.__proto__;
    let prototype = right.prototype
    while(true) {
        if(proto === null) return false
        if(proto === prototype) return true
        proto = proto.__proto__;
    }
}

手写ajax

ajax请求是通过js的xmlHttpRequest对象与后端服务器交换数据

var xhr = new XMLHttpRequest();
xhr.open("GET",url,false);
xhr.onreadtstatechange = function() {
  if(xhr.readystate == 4){
    //响应内容解析完成
     if(xhr.status == 200){
       //客户端请求成功
       alert(xhr.responseText);
     }
  } 
}

状态码说明:

0-未初始化,还未调用send方法 1-载入,已调用send方法发送请求,等待响应 2-载入完成,完全接收到响应内容 3-交互,正在解析响应内容 4-完成解析,可以进行调用

手写jsonp

jsonp原理:因为jsonp发送的并不是ajax请求,其实是动态创建script标签 script标签是没有同源限制的,把script标签的src指向请求的服务端地址。

 function jsonp (url,data={},callback='callback') {
     //处理json对象,拼接url
     data.callback = callbak
     let params = []
     for(let key in data){
         params.push(key + '=' + data[key])
     }
     let script = document.creatElement('script')
     script.src = url + '?' + params.join('&')
     document.body.appendChild(script)
     
     //返回Promise
     return new Promise ((resolve,reject) => {
        window[callback] = (data) => {
            try{
                resolve (data)
            } catch(e){
                reject(e)
            } finally {
                //移除script元素
                script.parentNode.removeChild(script)
                console.log(script)
            }
        }
     })
 }
 
 //请求数据
 jsonp('http://photo.sina.cn/aj/index',{
     page:1,
     cate:'recommend',
 },'jsoncallback').then(data => {
     console.log(data)
 })

防抖与节流

防抖

防抖的含义:当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01 秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。

// 防抖动函数
function debounce(fn,wait=50,immediate) {
    let timer;
    return function() {
        if(immediate) {
            fn.apply(this,arguments)
        }
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=> {
            fn.apply(this,arguments)
            timer = null;
        },wait)
    }
}

节流

可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次

手写节流

function throttle(fn, wait) {
	let prev = new Date();
	return function() { 
	  const args = arguments;
		const now = new Date();
		if (now - prev > wait) {
			fn.apply(this, args);
			prev = new Date();
		}
	}

函数防抖的使用场景:

  • 手机号、邮箱输入检测
  • 窗口大小resize

函数节流使用场景:

  • 滚动加载,加载更多或者滚到底部监听
  • 搜索框搜索联想功能
  • 高频点击提交、重复提交

手写js数组push、splice、map、foreach、find方法

push方法

Array.prototype.push = function () {
    for (var i = 0; i< arguments.length; i++) {
        this[this.length] = arguments[i]
    }
    return this.length
}

splice方法

Array.prototype._splice = function (index, howmany = 0) {
  let arr = this
  let left = arr.slice(0, index) // 截取左边的数组
  let right = arr.slice(index + howmany, arr.length) // 截取右边的数组
  let subArr = Array.prototype.slice.call(arguments, 2) // 截取参数里面需要添加的数组
  let result = []
  // 合并数组
  result = [...left, ...subArr, ...right]
  // 这里改变 this, 就是改变原数组
  for (let i = 0; i < result.length; i++) {
    this[i] = result[i]
  }
  // 返回删除的数据
  return this.slice(index, index + howmany)
}

map方法(reduce实现)

if (!Array.prototype.mapUsingReduce) {
  Array.prototype.mapUsingReduce = function(callback, thisArg) {
    return this.reduce(function(mappedArray, currentValue, index, array) {
      mappedArray[index] = callback.call(thisArg, currentValue, index, array)
      return mappedArray
    }, [])
  }
}

[1, 2, , 3].mapUsingReduce(
  (currentValue, index, array) => currentValue + index + array.length
) // [5, 7, , 10]

find方法

function find(arr, callback) => {
  for(let i = 0; i< arr.length;i++) {
    let res = callback(arr[i], i);
    if(res) {
			return arr[i]
    }
  }
  return undefined
}

Reduce方法

function reduce(arr, callback, initValue) {
  let result = initValue;
  for(let i=0; i < arr.length;i++) {
    result = callback(result, arr[i]);
  }
  
  return result
}

手写数组flat方法

数组拍平方法 Array.prototype.flat() 也叫数组扁平化、数组拉平、数组降维。

Array.prototype.flat = function () {
    var temp = []
    function recurision (arr) {
        for (var i = 0; i < arr.length; i++) {
            let item = arr[i]
            if (Array.isArray(item)) {
                recurision(item)
            } else {
                temp.push(item)
            }
        }
    }
    recurision(this)
    return temp
}

二维数组降维

var flattened = [[0,1],[2,3],[4,5],[6,7]].reduce(
    function(a,b){
      return a.concat(b)
    },
    []
)

手写jsonStringify

function jsonStringify(data) {
  let type = typeof data;
  if(type !== 'object') {
    let result = data;
    if (Number.isNaN(data) || data === Infinity) {
       //NaN 和 Infinity 序列化返回 "null"
       result = "null";
    } else if (type === 'function' || type === 'undefined' || type === 'symbol') {
      // 由于 function 序列化返回 undefined,因此和 undefined、symbol 一起处理
       return undefined;
    } else if (type === 'string') {
       result = '"' + data + '"';
    }
    return String(result);
  } else if (type === 'object') {
     if (data === null) {
        return "null"  
     } else if (data.toJSON && typeof data.toJSON === 'function') {
         // 对象可能内置toJSON方法来自定义序列化对象
        return jsonStringify(data.toJSON());
     } else if (data instanceof Array) {
        let result = [];
        data.forEach((item, index) => {
        if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
               result[index] = "null";
           } else {
               result[index] = jsonStringify(item);
           }
         });
         result = "[" + result + "]";
         return result.replace(/'/g, '"');
      } else {
         // 处理普通对象
         let result = [];
         Object.keys(data).forEach((item, index) => {
            if (typeof item !== 'symbol') {
              //key 如果是 symbol 对象,忽略
              if (data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') {
                //键值如果是 undefined、function、symbol 为属性值,忽略
                result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
              }
            }
         });
         return ("{" + result + "}").replace(/'/g, '"');
        }
    }
}

ES6、ES5数组去重

利用ES6:

set类型是不重复的空数组,将array转成set再转成array就行。

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))

双循环

function unique(arr){            
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))

建立一个空数组,利用indexof判断空数组中是否有元素,没有则逐项添加,最后返回新数组

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array .indexOf(arr[i]) === -1) {
            array .push(arr[i])
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))

利用array.reduce去重

let myArray = ['a','a','b','b','c','c','e','d','d','d','d','d']
let MyOrderedArray = myArray.reduce(function(accumulator,currentValue){
  if(accumulator.indexOf(currentValue)=== -1){
    accumulator.push(currentValue);
  }
  return accumulator
},[])

console.log(myOrderedArray)

`

let arr = [1,1,2,3,4,2,3,5,5,5]
let result = arr.sort().reduce((init,current)=>{
  if(init.length === 0 ||init[init.length-1] !== current){
     init.push(current);
  }
  return init;
},[])
console.log(result)

函数柯里化

把接收多个参数的函数转换为接收单一参数的函数

//普通函数
var add = function(x,y){
  return x+y
}
add(3,4)
let addCurry = curry(add)
//柯里化之后
addCurry(3)(4)

函数柯里化实现

function curry1(func) {
  return function curried(...args) {
    if (args.length == func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}

偏函数

偏函数是将n个参数的函数转换为固定x个参数的函数,剩余n-x个参数将在下次调用全部传入.

偏函数和函数科里化有点像,所以根据函数科里化的实现可以写出偏函数的实现

function partial(fn, ...args) {
  return (...arg) => {
    args[index] = 
      return fn(...args, ...arg)
  }
}

遍历深层次对象

给定一个对象,用给定str去遍历

const obj = {
   a:{
      b:{
        c:666
      }
   }
}

var str = 'a.b.c'

const getData = (obj,str)=>{
  str.split('.').forEach(element =>{
    obj = obj[element]
  })
  return obj;
}
 console.log(getData(obj,str))
)

function 

数字千分位加逗号

最简单:js内置的toLocaleString函数可以实现这个功能

其他:转换成字符数组,再循环遍历

function numFormat(num){
    num=num.toString().split(".");  // 分隔小数点
    var arr=num[0].split("").reverse();  // 转换成字符数组并且倒序排列
    var res=[];
    for(var i=0,len=arr.length;i<len;i++){
      if(i%3===0&&i!==0){
         res.push(",");   // 添加分隔符
      }
      res.push(arr[i]);
    }
    res.reverse(); // 再次倒序成为正确的顺序
    if(num[1]){  // 如果有小数的话添加小数部分
      res=res.join("").concat("."+num[1]);
    }else{
      res=res.join("");
    }
    return res;
}

正则表达式

function numFormat(num){
  var res=num.toString().replace(/\d+/, function(n){ // 先提取整数部分
       return n.replace(/(\d)(?=(\d{3})+$)/g,function($1){
          return $1+",";
        });
  })
  return res;
}

箭头函数不能作为构造函数的原因

构造函数是通过new 关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this,因此不能使用箭头函数作为构造函数,也不能通过new操作符来调用箭头函数

函数调用

函数调用有四种方式:

  1. 作为一个函数调用
  2. 函数作为方法调用
  3. 使用构造函数调用函数
  4. 作为函数方法调用函数(call、apply)

ES6语法转ES5语法的实现

ES6 转 ES5 目前行业标配是用 Babel,转换思路为:

1.把代码字符串解析成AST(Abstract Syntax Tree,抽象语法树)

2.AST就是一个json,按照一定规则把这个json里ES6部分的东西转换成ES5的

3.再把修改后的AST转换成代码

举例

function hello(a, b, c){ 
}
//解析为抽象语法树
{
  "type": "Program",
  "body": [
    {
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "hello"
      },
      "params": [
        {
          "type": "Identifier",
          "name": "a"
        },
        {
          "type": "Identifier",
          "name": "b"
        },
        {
          "type": "Identifier",
          "name": "c"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "body": []
      },
      "generator": false,
      "expression": false,
      "async": false
    }
  ],
  "sourceType": "script"
}

有了这个json后,就可以对代码进行操作了。在没有树的情况下,如果要对文件里的某一个语句进行替换的话,一般就是全局 搜索然后replace,这样有可能影响到别的代码,但是有了树后,就可以对这个json进行操作,精确地去修改某个对象的属性,也就不会影响到别的代码了。所以babel转换ES6的核心,就是在ast中按照一定的规则取修改json里的属性和方法,然后再把tree转换成代码

html解析器

https://cloud.tencent.com/developer/article/1842858

https://juejin.cn/post/6844904048194224135

https://blog.csdn.net/qq_38819293/article/details/122231471

leetcode 1410 https://juejin.cn/post/6844904122282409997?from=search-suggest

https://juejin.cn/post/7089621605826953247

js实现红绿灯

使用setTimeout、Promise、async await 三种方式实现红绿灯代码,红灯2秒,黄灯1秒,绿灯3秒,循环改变颜色。改变颜色的方法,就简单写成打印出颜色。

function changeColor(color) {
	console.log('traffic-light ', color);
}
function main() {
	changeColor('red');
	setTimeout(()=>{
		changeColor('yellow');
		setTimeout(() => {
			changeColor('green');
			setTimeout(main, 2000);
		}, 1000);
	}, 2000);
}
main();

Async await实现

function sleep(duration) {
	return new Promise(resolve => {
		setTimeout(resolve, duration);
	})
}
async function changeColor(color, duration) {
	console.log('traffic-light ', color);
	await sleep(duration);
}
async function main() {
	while (true) {
		await changeColor('red', 2000);
		await changeColor('yellow', 1000);
		await changeColor('green', 3000);
	}
}
main();

使用Promise,把下一次的颜色改变写在then里面,最后同样使用递归完成循环。

function sleep(duration){
    return new Promise(resolve => {
        setTimeout(resolve, duration);
    })
}
function changeColor(duration,color){
    return new Promise(resolve => {
		console.log('traffic-light ', color);
    	sleep(duration).then(resolve);
	})
}
function main() {
	return new Promise(resolve => {
		changeColor(2000, 'red').then(() => {
			changeColor(1000, 'yellow').then(() => {
				changeColor(3000, 'green').then(() => {
					main();
				})
			})
		})
	})
}
main();

连线

通过canvas划线

https://www.jianshu.com/p/bd55a625b839

js判断数组方法比较

typeof无法直接返回数组,

tostring方法

使用array对象的isarray方法

实现一个enum

const Color = {
    RED: 'red',
    GREEN: 'green',
    BLUE: 'blue'
}

Object.freeze(Color);

class Color {
    constructor(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
}

Color.RED = new Color('xxx');
Color.GREEN = new Color('green');
Color.BLUE = new Color('blue');

Object.freeze(Color);  

浏览器兼容问题

笔试资源

js笔试:Https://www.huaweicloud.com/articles/2774625590336c3fe79e6468b956c6a3.html

前端面试:https://github.com/haizlin/fe-interview

淘宝大佬前端:https://github.com/mqyqingfeng/Blog

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github