Bootstrap

2021JavaScript面试题(最新)不定时更新(2021.11.6更新)

插个小广告~
字节跳动前端开发工程师-番茄小说内推,校招、社招、实习均可。
欢迎加我q:2679330388,欢迎来撩~

文章目录


说一下JS的基本数据类型

JavaScript的数据类型分为俩种,一种是基本数据类型,一种是引用数据类型

1.基本数据类型

js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型。
Symbol 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。

2.引用数据类型

引用数据类型统称为 Object 对象,主要包括对象、数组、函数、日期和正则等等。

  • Symbol类型是做什么的?
null 和 undefined 的区别?

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

介绍一下JavaScript 原型、原型链?原型链有什么特点?_

**构造函数原型:**每一个构造函数的内部都有一个 prototype 属性,这个属性时一个指针,指向另一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

**对象原型:**当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,这个指针称为对象的原型。

原型链

当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype,然后Object.prototype.__proto _ _为null。

image-20210407210035841

Array 构造函数只有一个参数值时的表现?

Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度这样创建出来的只是一个空数组,只不过它的 length 属性被设置成了指定的值。

说一说其他值到字符串的转换规则?toString
  • null 和 undefined 类型 ,null 转换为 “null”,undefined 转换为 “undefined”,
  • Boolean 类型,true 转换为 “true”,false 转换为 “false”。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误
  • 对象,会调用Object.prototype.toString()来返回内部属性 [[Class]] 的值,如"[object Object]"。
如何把对象转换成字符串/字符串和对象的相互转换。

对象转字符串 JSON.stringify(obj)
字符串转对象或数组 JSON.parse(str)

其他值到布尔类型的值的转换规则?

利用Boolean对象进行转换

以下这些是假值:

  • undefined
  • null
  • 0和 NaN
  • “”

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

其他值到数字值的转换规则?

Number()函数的转换规则

  • null,返回 0。

  • undefined,返回 NaN。

  • 布尔值,true 转换为 1,false 转换为 0。

  • 数值,直接返回。

  • 字符串:

    • 只有数字,直接转
  • 空字符串,0

    • 其他情况,则返回 NaN。
  • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。

相等操作符 == 比较规则

1、两个简单数据类型,类型不同,转数字比较。

2、一个简单、一个复杂,复杂转简单后比较。

3、两个复杂,比地址。

{} 和 [] 的 valueOf 和 toString 的结果是什么?

valueOf()方法会将对象转换为基本类型,如果无法转换为基本类型,则返回原对象。

toString返回当前对象的字符串形式。

{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"

[] 的 valueOf 结果为 [] ,toString 的结果为 ""
判断数据类型的方法

基本类型判断

在 JavaScript 里 使用 typeof 来判断数据类型,只能区分基本类型,即 “ number ” , “ string ” , “ undefined ” , “ boolean ” , “ object ” ,"function"六种。

typeof '';               // string 有效
typeof 1;                // number 有效
typeof true;             // boolean 有效
typeof undefined;        // undefined 有效
typeof null;             // object 无效
typeof [] ;              // object 无效
typeof new Function();   // function 有效
typeof new Date();       // object 无效
typeof new RegExp();     // object 无效

引用类型判断

区别对象、数组、函数可以使用Object.prototype.toString.call 方法。判断某个对象值属于哪种内置类型。

console.log(Object.prototype.toString.call(123))          // [object Number]
console.log(Object.prototype.toString.call('123'))        // [object String]
console.log(Object.prototype.toString.call(undefined))    // [object Undefined]
console.log(Object.prototype.toString.call(true))         // [object Boolean]
console.log(Object.prototype.toString.call({}))           // [object Object]
console.log(Object.prototype.toString.call([]))           // [object Array]
console.log(Object.prototype.toString.call(function(){})) // [object Function]
console.log(Object.prototype.toString.call(this));        // [object Window]

对象类型判断

instanceof 运算符也常常用来判断对象类型。用法: 左边的运算数是一个object,右边运算数是对象类的名字或者构造函数; 返回truefalse

[] instanceof Array; // true
[] instanceof Object; // true
[] instanceof RegExp; // false
new Date instanceof Date; // true
判断一个变量是否为数组

Array.isArray
Object.prototype.toString.call()
x instanceof Array

Object.prototype.toString的原理

在 Object.prototype.toString 方法被调用时,他会找this对象的[[Class]]属性的值,将这个值与object字符串拼接并返回。

[[Class]]是一个内部属性,所有的对象都拥有该属性。在规范中,[[Class]]的值是一个字符串值,表明了该对象的类型。

typeof null输出什么

输出“object",为什么?

因为不同的对象在底层都表示为二进制,在Javascript中二进制前三位都为0的话会被判断为Object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回"object"。

typeof NaN 的结果是什么?

typeof NaN; // “number”

NaN 意指“不是一个数字”(not a number),用于指出数字类型中的错误情况。

NaN != NaN为 true。 NaN==NaN为false

typeof的原理

typeof原理:不同的对象在底层都表示为二进制,在Javascript中二进制低三位存储其类型信息。

  • 000: 对象
  • 001: 整数
  • 010: 浮点数
  • 100:字符串
  • 110: 布尔
instanceof的原理

Instanceof 是通过原型链去查找,找到某个对象的原型对象,使用原型对象的constructor找到构造函数,看看构造函数与Instanceof后面的是否相同,不相同,继续向上查找,直到尽头,找到为True,没找到为false。

0.1 + 0.2 等于多少

0.3000000000000004。不会精确等于0.3。

首先,十进制的0.1和0.2会被转换成二进制的,二进制浮点数表示法并不能精确的表示类似0.1这样的数值,因为浮点数在转化为二进制时,会出现无限循环

0.1 -> 0.0001 1001 1001 1001...(1100循环)
0.2 -> 0.0011 0011 0011 0011...(0011循环)

两者相加之后得到二进制为再转换为十进制,会产生误差。

我想0.1+0.2 精确的等于0.3怎么办

num.toFixed(小数点位数)
先✖️10相加 再除10

小数如何取整,或保留几位小数

可以使用toFixed方法,可把 Number 四舍五入为指定小数位数的数字。

var num = 5.56789;
var n=num.toFixed(2);

输出:

5.57
JavaScript 什么是闭包?

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

  • 闭包的用途
  • 闭包的应用场景
闭包的好处

闭包有两个常用的用途。

1.可以让我们函数外部能够访问到函数内部的变量。

2.闭包函数保留了使用的变量对象的引用,保证变量对象不会被回收。

闭包有很多好处,如果我们过多的使用,就会导致大量的变量都被保存在内存中,内存消耗很大,造成网页的性能问题,且容易造成内存泄漏。解决方法是在退出函数之前,将不使用的局部变量全部删除。(至为null这样)

// 解除引用避免内存泄露
function closure(){    
    var div = document.getElementById('div');    
    var test = div.innerHTML;
    div.onclick = function () {
        console.log(test);
    };
    div = null;
}
闭包的应用场景
  • 构造函数的私有属性
  • 函数节流、防抖

私有属性

在函数中使用var来创建变量,这时候在函数外部就无法获取到这个变量,我们可以在函数内部提供一个特权方法来访问这个变量。

function Person(param) {
    var name = param.name; // 私有属性
    this.age = 18; // 共有属性

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

const tom = new Person({name: 'tom'});
tom.age += 1; // 共有属性,外部可以更改
tom.sayName(); // tom
tom.name = 'jerry';// 私有属性,外部不可获取、更改
tom.sayName(); // tom
!!!节流、防抖了解吗?写一下节流防抖

函数节流

节流是指在一段时间内只允许函数执行一次。我们可以使用定时器实现节流。

函数节流会用在比input, keyup更频繁触发的事件中,如resize, touchmove, mousemove, scroll。throttle 会强制函数以固定的速率执行

// 节流
var throttle = function(func, delay){
    var timer = null;

    return function(){
        var context = this;
        var args = arguments;
        if(!timer){
            timer = setTimeout(function(){
                func.apply(context, args);
                timer = null;
            },delay);
        }
    }
}

防抖

防抖的作用是,一个事件回调函数在一段时间后才执行,如果在这段时间内再次调用则重新计时。

实际应用场景: 搜索框,如果用户不断输入,通过防抖节约请求资源。

一般我是使用lodash的decounce进行防抖。

debounce(方法,时间)

在项目我开发一个备注搜索的时候,就是使用了防抖,避免频繁的发起请求。

原理:内部维护一个计时器timer,返回一个函数,在这个函数里清除计时器并重新计时,计时结束后执行fn函数。

// 防抖
function debounce(fn, delay){
    // 维护一个 timer
    let timer = null;
    
    return function() {
        // 获取函数的作用域和变量
        let context = this;
        let args = arguments;
        
        clearTimeout(timer);// 清除重新计时
        timer = setTimeout(function(){
            fn.apply(context, args);
        }, delay)
    }
}
Symbol类型是做什么的?

SymbolES6 新推出的一种基本类型,它表示独一无二的值。它最大的用途就是用来定义对象唯一的属性名,就能保证不会出现同名的属性,还能防止某一个属性被不小心覆盖。

Symbol的具体用法

通过Symbol()方法可以生成一个symbol,里面可以带参数,也可以不带参数

Symbol 类型的注意点

  • Symbol 函数前不能使用 new 命令,否则会报错。
  • Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
  • Symbol值不能与其他类型值运算,不能转数值;可以转字符串和布尔值
  • 不能用.运算符,要用方括号
  • Symbol 作为属性名时,该属性不会出现在 for…in、for…of 循环中,也不会被 Object.keys() 返回。
  • Object.getOwnPropertySymbols 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
  • Symbol.for 接受一个字符串作为参数,首先在全局中搜索有没有以该参数为名称的Symbol值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
Symbol和Symbol.for的区别

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

!!!列举ES6的新特性并说一下如何使用

1.const 与 let以及var以及块级作用域

  • var是函数作用域,会变量提升。
  • let是块级作用域,不会变量提升。
  • const用于定义常量,是块级作用域,不会变量提升

什么是块级作用域?

是比函数作用域更小的作用域,例如for循环、i语句的{}

  • 什么是块级作用域?

const是常量

const声明一个只读的常量,const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。

let和const没有变量提升,会存在暂时性死区

var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

比如,存在全局变量temp,但是块级作用域内let又声明了一个局部变量temp,局部变量就绑定这个块级作用域,所以在let声明变量前,对temp赋值会报错

2.箭头函数

ES6 中,箭头函数就是函数的一种简写形式,使用括号包裹参数,跟随一个 =>,紧接着是函数体;

具体的使用细节:

箭头函数和普通函数的区别

  • 箭头函数不能使用arguments、super和new.target,可以使用rest参数访问参数列表
  • 箭头函数没有prototype属性,不能用作构造函数
  • 箭头函数的this指向,定义箭头函数的上下文
  • 普通函数可以使用call修改this。但箭头函数不能用call、apply修改里面的this
  • new发生了什么/为什么箭头函数不能用new实例化对象

3.Promise(常用)

Promise的出现主要是为了解决回调地狱的问题。

具体的使用的话:

Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。

一个 Promise 实例有三种状态,分别是 pending、fulfilled 和 rejected,分别代表了进行中、成功和失败。实例的状态只能由进行中转变成功或者进行中转失败,并且状态一经改变,就无法再被改变了。

状态的改变是通过 resolve() 和 reject() 函数来实现的,我们可以在异步操作结束后调用这两个函数改变 Promise 实例的状态。

Promise的原型上定义了一个 then 方法, 分别是成功和失败的回调。我们可以使用这个 then 方法可以为两个状态的改变注册回调函数。

这样子我们创建了一个最基本的promise。

  • 实现一下Promise
  • 什么是回调地域

4.Generator

Generator 函数也是 ES6 提供的一种异步编程解决方案。

在第一次调用Generator函数的时候并不会执行函数内部的代码,而是会返回一个生成器对象。而每次调用next方法则将函数执行到下一个yield的位置,同时向外界返回yield关键字后面的结果。如此往复,直到Generator函数内部的代码执行完毕或return。

5.async 函数

async 函数是 Generator 函数的语法糖,async不再需要执行器,执行async函数,就会自动执行函数内部的逻辑。await后面既可以是promise也可以是任意类型的值,此时等同于同步操作。

async 函数的返回值是 Promise 对象,可以使用then方法添加回调函数

6.Proxy

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作。它是在目标对象之前设置了一层“拦截”,当该对象被访问的时候,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

具体的使用:

一个 Proxy 对象由两个部分组成: target 、 handler 。在通过 Proxy 构造函数生成实例对象时,需要提供这两个参数。 target是目标对象, handler 是一个对象,声明了代理 target 的指定行为。

7.模板字符串

ES6新增了模板字符串,用反引号(``)表示,可以用于定义多行字符串,或者在字符串中嵌入变量。如果要在模板字符串中嵌入变量,需要将变量名写在${}之中。

8.Module模块化

import导入,export导出。

  • 什么是模块化开发

9.解构赋值

我对于数组解构原理的理解是,从数组中提取值,按照对应位置,对变量赋值。对象解构原理的理解是,通过键找键,找到了相同的属性名就赋值了。

10.rest运算符

用…表示,两个功能:展开、收集。

  • 可以展开一个数组,放入另一个数组。
  • 放在最后,获取到剩余的元素放到数组中。
new发生了什么
  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象。
  3. 让函数的 this 指向这个对象,执行构造函数的代码。
  4. 判断函数return的是否为Object,若为Object则返回该object,否则返回创建的新对象
function create(Super) {
  let myObj = {}
  myObj.__proto__ = Super.prototype
  const res = Super.call(myObj)
  if (typeof res === 'object') return res
  else return myObj
}

因为箭头函数既没有prototype,也没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会改变,所以箭头函数不能作为构造函数使用,用new调用时会报错!

什么是回调地域

回调地狱就是多层嵌套的问题。 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性,需要多次异步请求的话,就会显得代码跳跃且乱。

实现一下Promise/Promise的原理

promise的实现原理,Promise内部有一个变量记录当前状态为pending、fulfilled还是rejected,还有两个队列保存成功的回调和失败的回调。

当在Promise中调用resolve之后,如果当前状态为pending,则状态由pending转fulfilled,记录传入值,并依次执行成功回调;若在Promise中调用reject,如果当前状态为pending,则状态由pending转rejected,记录传入值,并依次执行失败回调。这就是resolve和reject的实现原理。

then方法的话,它允许注册成功、失败两个回调函数,如果当前状态为pending,则分别放入成功、失败回调数组中;如果为fulfilled,则执行成功回调;如果是rejected,则执行失败回调,最后返回this。

TODO: 为了支持同步的Promise,这里采用异步调用???

// 判断变量否为function
const isFunction = (variable) => typeof variable === 'function'
// 定义Promise的三种状态常量
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(handler) {
    if (!isFunction(handler)) {
      throw new Error('MyPromise must accept a function as a parameter')
    }
    this._status = PENDING
    this._value = undefined
    this.fulfilledCbs = []
    this.rejectedCbs = []
    try {
      handler(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }
  //resolve的函数
  _resolve(value) {
    setTimeout(() => {
      if (this._status !== PENDING) return
      this.status = FULFILLED
      this.value = value
      this.fulfilledCbs.forEach((callback) => {
        callback(value)
      })
    }, 0)
  }
  //reject的函数
  _reject(err) {
    if (this._status !== PENDING) return
    setTimeout(() => {
      this._status = REJECTED
      this._value = err
      this.rejectedCbs.forEach((callback) => {
        callback(err)
      })
    }, 0)
  }
  // 添加then方法
  then(onFulfilled, onRejected) {
    const { _value, _status } = this
    // 返回一个新的Promise对象
    return new MyPromise((onFulfilledNext, onRejectedNext) => {
      // 封装一个成功时执行的函数
      let fulfilled = (value) => {
        if (!isFunction(onFulfilled)) {
          onFulfilledNext(value)
        } else {
          let res = onFulfilled(value)
          onFulfilledNext(res)
        }
      }
      // 封装一个失败时执行的函数
      let rejected = (error) => {
        if (!isFunction(onRejected)) {
          onRejectedNext(error)
        } else {
          let res = onRejected(error)
          onFulfilledNext(res)
        }
      }
      switch (_status) {
        // 当状态为pending时,将then方法回调函数加入执行队列等待执行
        case PENDING:
          this.fulfilledCbs.push(fulfilled)
          this.rejectedCbs.push(rejected)
          break
        // 当状态已经改变时,立即执行对应的回调函数
        case FULFILLED:
          fulfilled(_value)
          break
        case REJECTED:
          rejected(_value)
          break
      }
    })
  }
  // 添加catch方法
  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
  
}

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功啦')
    // reject('失败啦')
  }, 1000)
})
  .then((value) => {
    console.log(value)
  })
  .then((v) => {
    console.log('hahah', v)
  })
//定义状态常量
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  //构造方法
  constructor(handle) {
    this.status = PENDING;
    this.value = undefined;
    // 用于保存 resolve 的回调函数
    this.fulfilledCallbacks = [];
    // 用于保存 reject 的回调函数
    this.rejectedCallbacks = [];
    //执行handle
    try {
      handle(this._resolve.bind(this), this._reject.bind(this));
    } catch (error) {
      this._reject(err);
    }
  }
  //resolve的函数
  _resolve(value) {
    setTimeout(() => {
      if (this.status !== PENDING) return;
      this.status = FULFILLED;
      this.value = value;
      this.fulfilledCallbacks.forEach(callback => {
        callback(value)
      })
    }, 0)
  }

  //reject的函数
  _reject(err) {
    setTimeout(() => {
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this.value = err;
      this.rejectedCallbacks.forEach(callback => {
        callback(err);
      })
    }, 0);
  }

  // then方法
  then(onFulfilled, onRejected) {
    const { value, status } = this;
    // 返回一个新的Promise对象
    return new MyPromise((onFulfilledNext, onRejectedNext) => {
      switch (status) {
        case PENDING:
          this.fulfilledCallbacks.push(onFulfilled);
          this.rejectedCallbacks.push(onRejected);
          break;
        //状态改变,则立即执行对应的回调函数
        case FULFILLED:
          onFulfilled(value)
          break;
        case REJECTED:
          onRejected(value)
          break;
      }
    })
  }
    
  // 添加catch方法
  // 相当于调用 then 方法, 但只传入 Rejected 状态的回调函数
  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
  // 添加静态resolve方法
  static resolve(value) {
    // 如果参数是MyPromise实例,直接返回这个实例
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
  }
  // 添加静态reject方法
  static reject(value) {
    return new MyPromise((resolve, reject) => reject(value))
  }
  // 添加静态all方法
  static all(list) {
    return new MyPromise((resolve, reject) => {
      //返回值的集合
      let values = []
      let count = 0
      //entries() 方法返回一个数组的迭代对象,该对象包含数组的键值对 (key/value)。
	  //迭代对象中数组的索引值作为 key, 数组元素作为 value。
      for (let [i, p] of list.entries()) {
        // 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
        this.resolve(p).then(res => {
          values[i] = res
          count++
          // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
          if (count === list.length) resolve(values)
        }, err => {
          // 有一个被rejected时返回的MyPromise状态就变成rejected
          reject(err)
        })
      }
    })
  }
  // 添加静态race方法
  static race(list) {
    return new MyPromise((resolve, reject) => {
      for (let p of list) {
        // 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
        this.resolve(p).then(res => {
          resolve(res)
        }, err => {
          reject(err)
        })
      }
    })
  }
  
  // finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
  //无论成功失败都执行的函数,其实就是给then方法分别传入这个方法。使用resolve将执行后的函数结果进行包装,调用then方法,该return return 该抛出错误该抛出错误
  finally(cb) {
    return this.then(
      value => MyPromise.resolve(cb()).then(() => value),
      reason => MyPromise.resolve(cb()).then(() => { throw reason })
    );
  }  
}

call() 、apply() 和bind()的区别?

apply 、 call 、bind 三者都是用来改变this指向的。

call:接受一个上下文对象,参数列表,返回函数执行后的值。

apply:接受一个上下文对象,参数数组,返回函数执行后的值。

bind:接受一个上下文对象,参数列表,返回新函数。

  • 说一下this的指向
说一下this的指向

this的指向,在调用时才能确定。

this指向的优先级:箭头函数、new、call/apply/bind、对象.、直接调用

1、箭头函数的this,指向定义箭头函数的上下文,即指向外层的 this 。

2、new 对象,this 指向新创建的对象。

3、call、bind、apply修改this指向,指向这个传入的context上下文,不传指向window

4、通过obj.某函数调用,this指向Obj

5、在全局上下文中调用,非严格模式下,this指向windows,严格模式下指向undefined。如果不再全局,被谁调用 this 指向谁。

window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
function sayColor() { 
 console.log(this.color); // this指向windows
} 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor();// 'blue'

6、匿名函数、自动执行函数的this,指向window。

测试题

// 1、
function Pig(){
   this.age=99;
   return 'abc';
}
var pig=new Pig();
console.log(pig) 

// 2、
var a=1;
function printA(){
  console.log(this.a);
}
var obj={
  a:2,
  foo:printA,
  bar:function(){
    printA();
  }
}

obj.foo(); 
obj.bar(); 
var foo=obj.foo;
foo(); 

// 3、
var fullName = 'language'
var obj = {
  fullName: 'javascript',
  prop: {
    getFullName: function () {
      return this.fullName
    }
  }
}
console.log(obj.prop.getFullName())
var test = obj.prop.getFullName
console.log(test())

// 4、
var val = 1
var json = {
  val: 10,
  dbl: function () {
    val *= 2
  },
}
json.dbl()
console.log(json.val + val)

// 5、
var num = 10 
var obj = { num: 20 }
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n 
    num++
    console.log(num) 
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

// 6、
var obj = {
  _name: 'obj',
  func() {
    const arrowFunc = () => {
      console.log(this._name)
    }
    return arrowFunc
  },
}
obj.func()()
var func = obj.func
func()()
obj.func.bind({ _name: 'newObj' })()()
obj.func.bind()()()
obj.func.bind({ _name: 'bindObj' }).apply({ _name: 'applyObj' })()

答案:

1、Pig {age:99}

2、2、1、1

解释:

(1)obj.foo(),printA直接被obj调用,this指向obj。

(2)obj.bar(),bar的属性值是一个function,function中调用printA,没有明确哪个对象调用的printA,this默认指向全局window。

(3)foo(),将obj.foo赋予给foo,调用foo(),相当于调用window.foo(),this指向window。

3、undefined、language

4、12

解释:dbl方法中,没有使用this,此时的 val 为全局 val

5、22 23 65 30

解释:

(1)第三行 obj.fn 立即执行函数,将20传入,自动执行函数 this 指向 window ,此时全局 num 变为60内部num变为21,返回一个函数。

(2)fn(5),调用函数,此时在全局上下文中调用,this 执行window,**全局 num 变为65 **,内部num变为22输出22

(3)obj.fn(10),此时 this 执行 obj ,obj 的 num 变为30内部num变为23输出23

(4)最后输出全局num和obj.num,即65,30

6、obj、undefined、newObj、undefined、bindObj

解释:

(1)调用obj.func()(),箭头函数的this指向func的this,func的this为obj,故输出obj

(2)func(),func的指向为window,window没有_name,故输出undefined

(3)bind 一个对象,输出newObj

(4)bind没有指明上下文,则bind window,输出undefined

(5)多次修改this指向,只认第一次,输出bindObj

事件循环 Event loop

JS的整个执行过程,我们称为事件循环过程,这个过程中涉及执行栈和两个任务队列:宏任务、微任务。

宏任务包括:

1、script整体代码

2、setTimeout、setInterval

3、node环境的setImmediate

4、输入输出和UI render

微任务包括:

1、node环境的process.nextTick

2、Promise的回调函数

3、MutationObserver的回调

4、await后面的代码

执行过程:执行宏任务,执行该宏任务产生的微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

  • async函数里面await标记之前的语句和 await 后面的语句是同步执行的。
  • 后面的语句是异步的,丢进Micro
  • new Promise在实例的过程中执行代码都是同步进行的,只有回调.then()才是微任务。
  • 注意:Promise中resolve之后,代码还会执行,除非在resolve的同时return

Q:

setTimeout(() => {
  console.log('真的在300ms后打印吗?')
}, 300)

**A:**不一定是精确的300ms之后打印。因为300ms的setTimeout并不是说300ms之后立马执行,而是300ms之后被放入任务列表里面。等待事件循环,等待它执行的时候才能执行代码。

MutationObserve

用于观察DOM节点的变化。

var observe = new MutationObserver(function(mutations,observer){
})

observe对象有三个方法:observe、disconnect、taskRecords。

  • observe:设置观察目标,接受两个参数,target:观察目标,options:通过对象成员来设置观察选项
observe.observe(target,{ childList: true});
手写call 、apply 、bind方法

将函数this设置为context对象的属性,通过隐式绑定的方式调用函数,把context上的属性删了,并返回函数调用的返回值

call

Function.prototype._call = function (context = window, ...args) {
  //创建独一无二属性,以免覆盖原属性
  const key = Symbol();
  context[key] = this;
  //通过隐式绑定的方式调用函数
  const result = context[key](...args);
  //删除添加的属性
  delete context[key];
  //返回函数调用的返回值
  return result;
};

apply

// 第二个参数是数组
Function.prototype._apply = function (context = window, args = []) {
  const key = Symbol();
  context[key] = this;
  //通过隐式绑定的方式调用函数
  const result = context[key](...args);
  //删除添加的属性
  delete context[key];
  //返回函数调用的返回值
  return result;
};

bind

Function.prototype._bind = function (context, ...args) {
  const fn = this;
  return function newFn(...newArgs) {
    return fn.apply(context, [...args, ...newArgs])
  }
}

如何校验正确性:

let obj = {
  name: 'chenhuan',
  sayName: function () {
    console.log(this.name)
  },
}

let fn = obj.sayName
fn.bind(obj)()

深拷贝和浅拷贝

深拷贝是将一个对象从内存中完整的拷贝一份出来,开辟一个新的内存空间存放新对象,且修改新对象不会影响原对象

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

实现浅拷贝

1.Object.assign()

Object.assign() 方法可以把源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

3、Array.prototype.concat()

4、Array.prototype.slice()

5、函数库lodash的_.clone方法

6、展开运算符...

实现深拷贝

1、函数库lodash的_.cloneDeep方法

2、

JSON.parse(JSON.stringify());
let obj1 = {
  name: 'chenhuan',
  age: 18
}

let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
obj2.name = "liuboyi";
console.log(obj1);
//{name: 'chenhuan', age: 18}

3、递归深拷贝

判断当前值是否为对象

1、如果不是对象,直接返回

2、如果是对象,创建结果数组,遍历对象,对当前值进行递归拷贝。

//递归实现深拷贝
function cloneDeep(obj) {
  if (typeof obj == 'object') {
    let cloneTarget = Array.isArray(obj) ? [] : {};
    //递归拷贝
    for (let key in obj) {
      cloneTarget[key] = cloneDeep(obj[key]);
    }
    //返回递归拷贝后的结果
    return cloneTarget;
  } else {
    //直接返回cuo
    return obj;
  }
}
WeakMap和Map的区别,及其应用场景

Map 叫字典,以键值对的形式存储。

Map 的键可以为任意值,而 WeakMap 的键只能是弱引用对象,在进行垃圾回收的时候不会考虑 WeakMap 对对象的引用。

Map的应用场景:数组去重,数据存储

WeakMap应用场景:保存DOM节点关联元数据。

WeakSet和Set的区别,及其应用场景

Set 集合,与数组类似,但成员唯一且无序,无重复值。

Set 的元素可以为任意值,WeakSet 的元素只能为弱引用对象。

Set :数组去重,数据存储

WeakSet应用:给对象打标签,例如查询元素是不是被禁用。

Map与Set的区别

Set 叫做集合,Map 叫做字典。

Set类似于数组,但成员是唯一且无序的,没有重复的值。

Map类似于对象,以键值对的形式进行存储,键可以为任意类型。

Set的方法

**属性:**set.size获取个数

方法:

  • add(value):新增
  • delete(value):存在即删除集合中value
  • has(value):判断集合中是否存在 value

Set转数组

  • Array.from 方法可以将 Set 结构转为数组
  • 使用…剩余运算符

遍历

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach((item,key)=>{}):使用回调函数遍历每个成员
Map的方法

方法

  • set(key, value):向字典中添加新元素
  • get(key):通过键查找特定的数值并返回
  • has(key):判断字典中是否存在键key
  • delete(key):通过键 key 从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历

  • Map.prototype.forEach((item,key)=>{}):遍历 Map 的所有成员。
  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
数组扁平化

数组扁平化:将嵌套的数组进行降维处理。

可以使用数组拍平方法 Array.prototype.flat(),接受一个参数

  • 不传参数时,默认“拉平”一层,可以传入一个整数,表示想要“拉平”的层数。

  • 传入 <=0 的整数将返回原数组,不“拉平”

  • Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组

  • 如果原数组有空位,Array.prototype.flat() 会跳过空位。

实现数组拍平flat函数

实现思路:

1、遍历数组

2、判断当前数组是否为数组

3、展开一层数组

//拍平数组
Array.prototype._flat = function (num = 1) {
  //<=0不展开直接返回
  if (num <= 0) {
    return this;
  }
  let arr = [];
  this.forEach((item) => {
    if (Array.isArray(item)) {
      //展开
      arr = arr.concat(item._flat(num - 1));
    } else {
      arr.push(item);
    }
  })
  num--;
  return arr;
}

reduce拍平

Array.prototype._flat = function (num = 1) {
  if (num <= 0) {
    return this;
  }

  let arr = this;
  let res = arr.reduce((sum, cur) => {
    if (Array.isArray(cur)) {
      sum = sum.concat(cur._flat(num - 1));
    } else {
      sum.push(cur);
    }
    return sum
  }, []);
  num--;
  return res;
}

介绍一下遍历方法
  • for … of
  • for … in
  • forEach()
  • reduce()
  • map()

for … of

for…of 循环可以使用的范围包括数组、Set 和 Map 结构以及字符串等可遍历对象。

const arr = ['red', 'green', 'blue'];
for(let v of arr) {
  console.log(v); // red green blue
}

for … in

for…in 一般用于遍历对象,循环读取键名。

forEach()

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。

reduce()

reduce() 接收一个函数作为累加器,数组中的每个值(从左到右)开始获取,最终计算为一个值。

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

map()

map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。

map和foreach有什么区别

forEach一般只用来遍历数组,不改变其数据。

map用来遍历数组并改变数据,返回新数组。

1、forEach()方法不会返回执行结果,而是undefined;map()方法会得到一个新的数组并返回。

2、map能保证原始数组的不变。forEach 不能确保数组的不变性,只有你不改变数据,原数组才会不变。

怎么用栈去实现队列

建两个栈s1、s2,在出栈的时候把栈底元素取出来,入栈的时候正常入栈。

1、入栈请求时,将数据压入s1。

2、出栈请求时,判断s2栈是否为空,不为空则将s2栈顶元素出栈;若为空,则将s1中数据依次压入s2,再取s2栈顶元素。

attribute和property的区别

attribute 是 dom 元素在文档中作为 html 标签拥有的属性
property 是 dom 元素在 js 中作为对象拥有的属性。

对于 html 的标准属性来说,attribute 和 property 是同步的,是会自动更新的(input的value值除外),但是对于自定义的属性来说,他们是不同步的。

async 和 defer 的作用是什么?有什么区别?(浏览器解析过程)

如果不加 async 和 defer,代码执行到script就会立即加载和执行js脚本,阻塞文档解析。

  • defer是延迟执行脚本,async是异步执行脚本。
  • 加了defer属性后会立即加载脚本,但执行会推迟到文档渲染完成后。
  • 加了async属性是脚本加载完毕后立即执行,脚本加载的过程,并不妨碍页面中的其他操作,但脚本的执行会阻塞文档解析。
使用async/defer后的js脚本会阻塞文档的解析吗?

defer是在HTML解析完之后才会执行,不会阻塞文档的解析。

而 aysnc 它是加载完成后立即执行,也就是说,设置async属性的js脚本的加载不会阻塞文档的解析,但是他的执行会阻塞文档的解析。

Css会阻塞dom解析吗 为什么会阻塞渲染

css不会阻塞Dom的解析,但会阻塞Dom的渲染。

我认为这是浏览器的优化机制,因为你加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞DOM树渲染的话,那么当css加载完之后,DOM树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以干脆等css加载完后,再来渲染DOM。

css加载会阻塞js运行吗?

CSS的加载会阻塞JS运行。

调试JS的工具
  • log输出
  • Source断点调试
promise.all 和 promise.race的区别

promise.all 全成功才成功,有失败则失败。

promise.race 跟随第一个状态改变的状态。

promise.all是并行的,怎么封装promise做串行处理

并行:多个异步请求同时进行

串行:一个异步请求完了之后在进行下一个请求

**实际场景:**有三个方法,方法一、方法二、方法三,需要执行完方法一之后执行方法二,执行完方法二之后执行方法三。

var p = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log('1000')
      resolve()
    }, 1000)
  })
}
var p1 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log('2000')
      resolve()
    }, 2000)
  })
}
var p2 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log('3000')
      resolve()
    }, 3000)
  })
}

思路1:then方法

1、执行第一个promise,在第一个promise状态改变后,也就是在then方法中,返回第二个promise并执行,同理。

p().then(() => {
  return p1()
}).then(() => {
  return p2()
}).then(() => {
  console.log('end')
})

思路2:async

var fn=async function(arr){
    for(let i=0,len=arr.length;i<len;i++){
        var result=await arr[i]
        console.log(result)
    }
}   
fn(arr)
封装一个异步类,实现只能有两个Promise在执行,该如何实现?/限制promise的并发数/ JS 请求调度器

思路

  1. 类中有max、count、一个quene队列保存超出数量限制待执行的任务
  2. 一个addTask方法用来添加任务,该方法返回一个Promise,为了让外面感知,该任务什么时候执行完毕。addTask中获取一个待执行任务的函数,并判断count是否少于max,少于则执行,不少于则放入等待队列。
  3. 一个getTask方法用来返回待执行任务的函数,在函数中执行数量+1,随后执行任务,使用finally方法,无论成功失败,执行完后count-1,此时腾出位置,判断等待队列是否有任务,若有任务,则取出执行,并在finally执行完毕之后
class limitRun {
  constructor(maxNum) {
    this.max = maxNum
    this.count = 0
    this.quene = []
  }
  //添加任务
  addTask(p, ...args) {
    // 为什么return Promise 为了让外面感知 这个任务什么时候能执行完毕
    return new Promise((resolve, reject) => {
      //获取待执行任务
      let task = this.getTask(p, args, resolve, reject)
      if (this.count < this.max) {
        console.log('run')
        task()
      } else {
        console.log('wait')
        this.quene.push(task)
      }
    })
  }
  //获取待执行任务的函数 返回一个函数
  getTask(p, args, resolve, reject) {
    return () => {
      //执行数量+1
      this.count++
      p(...args)
        .then(resolve)
        .catch(reject)
        .finally(() => {
          //无论执行成功或失败,执行完毕-1
          this.count--
          // 空位置腾出 如果有排队 重新拿出任务执行
          if (this.quene.length) {
            console.log('get from quene')
            let task = this.quene.shift()
            task()
          }
        })
    }
  }
}

let limit = new limitRun(3)

let p = function (delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(delay)
      resolve()
    }, delay)
  })
}

for (let i = 1; i <= 5; i++) {
  limit.addTask(p, i * 1000).then(() => {
    console.log(`${i}个任务执行完毕`)
  })
}

最后输出:
run
run
run
wait
wait
1000
第1个任务执行完毕
get from quene
2000
第2个任务执行完毕
get from quene
3000
第3个任务执行完毕
4000
第4个任务执行完毕
5000
第5个任务执行完毕

不用循环,实现abc *5的字符串的一个连接。
function strn(str, n) {
  return str.repeat(n);
}

console.log(strn("abc", 5));//abcabcabcabcabc
说一下Array的内置的API

增删 push pop shift unshift splice

查找 indexOf lastIndexOf includes find findIndex some every

拷贝 / 连接 concat slice

筛选 /遍历 filter map reduce forEach

其他 join reverse fill Array.isArray Array.from

concat() 连接两个或更多的数组,并返回结果。

copyWithin() 从数组的指定位置拷贝元素到数组的另一个指定位置中。

语法

array.copyWithin(target, start, end)

参数

参数描述
target必需。复制到指定目标索引位置。
start可选。元素复制的起始位置。
end可选。停止复制的索引位置 (默认为 array.length)。如果为负值,表示倒数。
实例
复制数组的前面两个元素到后面两个元素上:
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.copyWithin(2, 0);
fruits 输出结果:
Banana,Orange,Banana,Orange

entries() 返回数组的可迭代对象。

every() 检测数值元素的每个元素是否都符合条件。

  • 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
  • 如果所有元素都满足条件,则返回 true。
array.every(function(currentValue,index,arr), thisValue)
参数描述
thisValue可选。对象作为该执行回调时使用,传递给函数,用作 “this” 的值。 如果省略了 thisValue ,“this” 的值为 “undefined”

fill() 使用一个固定值来填充数组。

filter() 检测数值元素,并返回符合条件所有元素的数组。

find() 返回通过测试(函数内判断)的数组的第一个元素的值。

find() 方法为数组中的每个元素都调用一次函数执行:

  • 当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。
  • 如果没有符合条件的元素返回 undefined
array.find(function(currentValue, index, arr),thisValue)

findIndex() 返回传入一个测试条件(函数)符合条件的数组第一个元素位置。

findIndex() 方法为数组中的每个元素都调用一次函数执行:

  • 当数组中的元素在测试条件时返回 true 时, findIndex() 返回符合条件的元素的索引位置,之后的值不会再调用执行函数。
  • 如果没有符合条件的元素返回 -1

forEach() 数组每个元素都执行一次回调函数。

from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。

//类数组对象转换为数组:
let obj = {
  0: 'ch',
  1: 'en',
  2: 'sex',
  'length': 3
}

let arr1 = Array.from(obj);
console.log(arr1);//[ 'ch', 'en', 'sex' ]

//将Set结构的数据转换为真正的数组:
let arr = [12,45,97,9797,564,134,45642]
let set = new Set(arr)
console.log(Array.from(set))  // [ 12, 45, 97, 9797, 564, 134, 45642 ]

//将字符串转换为数组:
let  str = 'hello world!';
console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]


一个类数组对象转换为一个真正的数组,必须具备以下条件:

1、该类数组对象必须具有 length 属性,用于指定数组的长度。如果没有 length 属性,那么转换后的数组是一个空数组。

2、该类数组对象的属性名必须为数值型或字符串型的数字

indexOf() 搜索元素在数组的位置,从前往后查找,返回它所在的位置,没找到返回-1。接收两个参数:查找元素、开始查找位置。

lastIndexOf() 搜索元素在数组的位置,从后往前找,返回它所在的位置,没找到返回-1。接收两个参数:查找元素、开始查找位置。

includes() 判断一个数组是否包含一个指定的值。如果是返回 true,否则false。接收两个参数:查找元素、开始查找位置。

isArray() 判断对象是否为数组。

join() 把数组的所有元素放入一个字符串。

map() 通过指定函数处理数组的每个元素,并返回处理后的数组。

pop() 删除数组的最后一个元素并返回删除的元素。

push() 向数组的末尾添加一个或更多元素,并返回新的长度。

reduce() 接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

参数

参数描述
function(total,currentValue, index,arr)必需。用于执行每个数组元素的函数。 total:必须,初始值,或者计算结束后的返回值。currentValue:必需,当前元素。currentIndex:可选,当前元素的索引。arr:可选,当前元素所属的数组对象。
initialValue可选。传递给函数的初始值

reduceRight() 和reduce()功能一样,只不过是从右往左。

reverse() 反转数组的元素顺序。

shift() 删除并返回数组的第一个元素。

slice(start,[end]) 选取数组的一部分,并返回一个新数组。

some() 检测数组元素中是否有元素符合指定条件。

  • 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
  • 如果没有满足条件的元素,则返回false。

sort() 对数组的元素进行排序。

排序顺序可以是字母或数字,并按升序或降序。

默认排序顺序为按字母升序。

splice(index,deletenum,additem1,additem2…) 从数组中添加或删除元素。

toString() 把数组转换为字符串,并返回结果,数组中的元素之间用逗号分隔。

unshift() 向数组的开头添加一个或更多元素,并返回新的长度。

valueOf() 返回数组对象的原始值。

说一下字符串的内置的API
记忆顺序:
查找、转换、拷贝/连接、填充、匹配。其他。

查找 indexOf lastIndexOf charAt includes

转换 toLowerCase toUpperCase

拷贝 / 连接 concat slice substr substring repeat

填充 padStart(len,str) padEnd(len,str)

其他 split trim

匹配 match search replace

1、查找

str.charAt(index) 获取指定位置的字符

indexOf() 返回某个指定的字符串值在字符串中首次出现的位置

lastIndexOf() 从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置

includes() 查找字符串中是否包含指定的子字符串。

startsWith(x,[startindex])以x字符串开头,true/false。第二个参数为开始搜索位置。

endsWith(x,[endindex])以x字符串结尾,true/false。第二个参数表示字符串末尾位置。

2、转换

str.toLowerCase() 所有字符都转成小写

str.toUpperCase() 所有的字符都转成大写

3.字符串连接与截取

concat() 连接两个或更多字符串,并返回新的字符串。

slice(start,end) 提取字符串的片断,并在新的字符串中返回被提取的部分。 可以为负的整数

substr() 从起始索引号提取字符串中指定数目的字符。 非负的整数

substring() 提取字符串中两个指定的索引号之间的字符。 非负的整数

repeat() 复制字符串指定次数,并将它们连接在一起返回。

4、字符串填充

padStart(len,str)复制字符串,小于指定长度,在左边填充字符,直至长度满足,两个参数:长度、填充字符串(默认空格,可多个字符)。

padEnd(len,str)在右边填充

let stringValue = "foo"; 
console.log(stringValue.padStart(6)); // " foo" 
console.log(stringValue.padStart(9, ".")); // "......foo" 
console.log(stringValue.padEnd(6)); // "foo " 
console.log(stringValue.padEnd(9, ".")); // "foo......"

console.log(stringValue.padStart(8, "bar")); // "barbafoo" 
console.log(stringValue.padStart(2)); // "foo"

5、字符串分割

split() 把字符串分割为字符串数组,接受两个参数,第一个为分隔符/正则,第二个为数组大小。

let colorText = "red,blue,green,yellow"; 
let colors1 = colorText.split(","); // ["red", "blue", "green", "yellow"] 
let colors2 = colorText.split(",", 2); // ["red", "blue"] 
let colors3 = colorText.split(/[^,]+/); // ["", ",", ",", ",", ""]

6、其他

trim() 去除字符串前后的空白

valueOf() 返回某个字符串对象的原始值(基本数据类型,如果没有,返回对象本身)。

7、字符串模式匹配

search() 查找与正则表达式相匹配的值,返回第一个匹配的位置索引,没找到返回-1。

match() 查找找到一个或多个正则表达式的匹配,将会把所有的匹配打包成一个数组返回。

replace(字符串/RegExp对象,字符串或函数)

let text = "cat, bat, sat, fat"; 
let result = text.replace("at", "ond"); 
console.log(result); // "cond, bat, sat, fat" 
result = text.replace(/at/g, "ond"); 
console.log(result); // "cond, bond, sond, fond"
  • 第二个参数是字符串的情况下,有几个特殊的字符序列,可以用来插入正则表达式操作的值。

    $n 匹配第 n 个捕获组的字符串。

    let text = "cat, bat, sat, fat"; 
    result = text.replace(/(.at)/g, "word ($1)"); 
    console.log(result); // word (cat), word (bat), word (sat), word (fat)
    
  • replace()的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到 3 个参数:与整个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串。在有多个捕获组的情况下,每个匹配捕获组的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和原始字符串。这个函数应该返回一个字符串,表示应该把匹配项替换成什么。使用函数作为第二个参数可以更细致地控制替换过程

    function htmlEscape(text) { 
         return text.replace(/[<>"&]/g, function(match, pos, originalText) { 
             switch(match) { 
                 case "<": 
                 	return "&lt;"; 
             	case ">": 
             		return "&gt;"; 
            	 case "&": 
             		return "&amp;"; 
            	 case "\"": 
            		 return "&quot;"; 
             } 
         }); 
    } 
    console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>")); 
    // "&lt;p class=&quot;greeting&quot;&gt;Hello world!</p>"
    

matchAll() 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。

8、字符串迭代和解构

[…str]会变成元素为单个字符的数组

事件流

事件流一共由三个阶段分别是:1.捕获阶段 2.目标阶段 3.冒泡阶段

介绍一下事件冒泡和捕获

事件冒泡是指,事件会从最内层的元素开始发生,一直向上传播,直到document对象。

不支持冒泡的事件:blur、focus、mouseenter、mouseleave

事件捕获与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。

在进行项目开发的时候,我们可以自行选择事件处理函数在哪一个阶段被调用。addEventListener方法用来素绑定事件处理的函数,它的第三个参数允许我们设置为捕获阶段(true)或冒泡阶段(false)触发,默认冒泡。

事件冒泡和事件捕获过程图:

image-20210406141845729

1-5是捕获过程,5-6是目标阶段,6-10是冒泡阶段;

  • 如何阻止冒泡
事件代理/委托

事件代理是将事件监听函数加到父元素上,利用事件冒泡来处理多个子元素相同事件的方式。

因为在事件冒泡中,子元素的事件触发会冒泡到父元素中,触发父元素相同的事件。所以我们只需给父元素添加事件监听即可。

如果我们不使用事件代理,那需要遍历父元素下的子元素,挨个进行事件监听。当元素量很大的时候,会影响页面性能。

事件代理/委托有什么作用,主要用于哪些场景

作用:不用给多个子元素添加事件监听函数,只给父元素添加。

应用场景:

  • 当要给一组子元素添加相同事件时,可以直接添加给父元素。

最经典的应用就是,一个列表会不断有新的数据的添加,如果每添加一个数据就生成一个li,就需要添加一个新的监听,代码结构就特别不好,这时候使用事件代理就很方便。

如何阻止冒泡
  • 给子级加 event.stopPropagation( )/IE8以下 cancelBubble

  • 在事件处理函数中返回 false

如何阻止默认事件
  • event.preventDefault( )

  • return false

触发事件的方法

如果我们是使用on+type的方法添加的事件监听,可以直接使用xx.click()触发。

如果是addEventListener,我们可以使用new Event创建一个事件对象,参数为要触发的事件,然后使用触发对象.dispatchEvent(事件对象)触发。

let event = new Event("click");
elem.dispatchEvent(event);
说一下鼠标事件和键盘事件有哪些?

鼠标事件
click
dbllick
mousedown
mouseup
mousemove
mouseenter
mouseleave
mouseover
mouseout

键盘事件
keydown
keyup
keypress

对作用域的理解

作用域包括全局作用域、函数作用域、块级作用域。

全局作用域中创建的变量,在任意地方都可以访问。

函数作用域中的变量,只有在函数内部才能访问。

块级作用域中的变量,只能在当前块中进行访问

对作用域链的理解(变量的一个搜索过程)

所谓作用域链,其实就是我们在某个作用域中获取某个变量的值,如果在该函数中没有该变量的定义,则会到创建这个函数的那个域寻找,如果也没有,就会一层一层的向上寻找,直到找到全局作用域还是没有找到,就结束。

这个一层层搜索的过程就是作用域链。

js怎么实现继承,优缺点

一、原型链继承

将父类的实例作为子类的原型,这样根据原型链,子类就可以访问到父类的属性和方法。

function Parent() {
   this.isShow = true
   this.info = {
       name: "yhd",
       age: 18,
   };
}

Parent.prototype.getInfo = function() {
   console.log(this.info);
   console.log(this.isShow); // true
}

function Child() {};
Child.prototype = new Parent();

let Child1 = new Child();
Child1.info.gender = "男";
Child1.getInfo();  // {name: "yhd", age: 18, gender: "男"}

let child2 = new Child();
child2.getInfo();  // {name: "yhd", age: 18, gender: "男"}
child2.isShow = false

console.log(child2.isShow); // false

优点:

1、父类方法可以复用

缺点:

  1. 父类的所有引用属性(info)会被所有子类共享,更改一个子类的引用属性,其他子类也会受影响
  2. 子类型实例不能给父类型构造函数传参

二、构造函数继承

通过使用call()apply()方法,Parent构造函数在为Child的实例创建的新对象的上下文执行了,就相当于新的Child实例对象上运行了Parent()函数中的所有初始化代码,结果就是每个实例都有自己的info属性。

function Parent() {
  this.info = {
    name: "yhd",
    age: 19,
  }
}

function Child() {
    Parent.call(this)
}

let child1 = new Child();
child1.info.gender = "男";
console.log(child1.info); // {name: "yhd", age: 19, gender: "男"};

let child2 = new Child();
console.log(child2.info); // {name: "yhd", age: 19}

相比于原型链继承,构造函数继承可以给父类构造函数传参。

function Parent(name) {
    this.info = { name: name };
}
function Child(name) {
    //继承自Parent,并传参
    Parent.call(this, name);
    
     //实例属性
    this.age = 18
}

let child1 = new Child("yhd");
console.log(child1.info.name); // "yhd"
console.log(child1.age); // 18

let child2 = new Child("wxb");
console.log(child2.info.name); // "wxb"
console.log(child2.age); // 18

优点:

  1. 可以在子类构造函数中向父类传参数
  2. 父类的引用属性不会被共享

缺点:

  1. 子类不能访问父类原型上的方法(即不能访问Parent.prototype上定义的方法),因此所有方法属性都写在构造函数中,每次创建实例都会初始化

三、组合继承

组合继承综合了原型链继承和构造函数继承,将两者的优点结合了起来。

基本的思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性,这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

function Parent(name) {
   this.name = name
   this.colors = ["red", "blue", "yellow"]
}
Parent.prototype.sayName = function () {
   console.log(this.name);
}

function Child(name, age) {
   // 继承父类属性
   Parent.call(this, name)
   this.age = age;
}
// 继承父类方法
Child.prototype = new Parent();

Child.prototype.sayAge = function () {
   console.log(this.age);
}

let child1 = new Child("yhd", 19);
child1.colors.push("pink");
console.log(child1.colors); // ["red", "blue", "yellow", "pink"]
child1.sayAge(); // 19
child1.sayName(); // "yhd"

let child2 = new Child("wxb", 30);
console.log(child2.colors);  // ["red", "blue", "yellow"]
child2.sayAge(); // 30
child2.sayName(); // "wxb"

复制代码

优点:

  1. 父类的方法可以复用
  2. 可以在Child构造函数中向Parent构造函数中传参
  3. 父类构造函数中的引用属性不会被共享

四、原型式继承

一个对象作为创建对象的原型

function object(o) {
  // 创建临时类
  function f() { };
  // 修改类的原型为o, 于是f的实例都将继承o上的方法
  f.prototype = o;
  return new f();
}

let o = {
  name: 'sss',
  friends: ["1", "2", "3"]
}
let a = new object(o);
a.name = "Gss";
a.friends.push("Lihua");

let b = new object(o);
b.name = "SKK";
b.friends.push("HAH");

console.log(o.friends);//[ '1', '2', '3', 'Lihua', 'HAH' ]

缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

五、寄生式继承

通过原型式继承的方法创建一个实例,然后为这个实例添加属性和方法,最后返回这个实例。

function createObject(o) {
  let myobj = Object.create(o);
  myobj.sayname = function () {
    console.log(this.name);
  }
  return myobj;
}

let obj = {
  name: '123',
  friends: [1, 2, 3],
  say: function () {
    console.log('hi');
  }
}

let a = createObject(obj);
a.name = 'xixi';
a.friends.push(4);

let b = createObject(obj);
b.name = 'lala';
b.friends.push(5);

b.sayname()//lala

五、寄生组合继承

寄生组合式继承在吸取了组合式继承的优点上,避免了在子函数的原型上面创建不必要的、多余的属性

function inherit(child, parent) {
  let prototype = Object.create(parent.prototype); // 创建父类原型对象
  prototype.constructor = child; // 增强对象 
  child.prototype = prototype; // 赋值父类原型对象给子类原型
}

function Father(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
  console.log(this.name);
};
function Child(name, age) {
  Father.call(this, name);
  this.age = age;
}
inherit(Child, Father);
Child.prototype.sayAge = function () {
  console.log(this.age);
};

let s = new Child('ch', 19);
s.sayName()

六、extends

写代码,输出0-4,每隔一秒输一个

ES6

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000 * i)
}

ES5

// 形成闭包
for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, 1000 * i)
  })(i)
}

aysnc

let sayNum = function (n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(n);
      resolve();
    }, 1000);
  })
}

async function run() {
  for (let i = 0; i < 5; i++) {
    await sayNum(i);
  }
}

run()
手写数组的map函数
Array.prototype.myMap = function(callback, context) {
    var arr = this;
    var res = [];
    context = context ? context : window;
    for(let i = 0; i < arr.length; i++) {
        let tem = callback.call(context, arr[i], i, arr);
        res.push(tem); 
    }
    return res;
}

//map是一个函数,接受一个函数作为参数,这个函数又接受一个item index arr
Array.prototype._map = function (fn, context) {
  const res = []
  for (let i = 0; i < this.length; i++) {
    //对每一个执行fn函数后 返回fn的返回值
    res.push(fn.call(context, this[i], i, this))
  }
  return res
}

let arr = [1, 2, 3]
console.log(arr._map((item) => item * 2)) //[ 2, 4, 6 ]
手写数组的reduce函数
//reduce
Array.prototype._reduce = function (fn) {
  //如果没有提供sum,将第一个元素作为sum
  const arr = this;
  let res;
  if (arguments.length < 2 && arr.length == 1) return arr[0];
  if (arguments.length > 1) res = arguments[1];
  else res = arr.shift();
  //累加
  for (let i = 0; i < arr.length; i++) {
    res = fn(res, arr[i], i, arr);
  }
  return res;
}
Array.prototype._reduce = function (fn, initValue) {
  //累加器
  let sum = initValue
  if (!initValue) sum = this.shift()
  //累加
  for (let i = 0; i < this.length; i++) {
    sum = fn(sum, this[i], i, arr)
  }
  return sum
}
let arr = [1, 2, 3]
console.log(
  arr._reduce((sum, item, index, arr) => {
    return sum + item
  })
)
//6

js是单线程的,为什么js能有异步任务?

JS是单线程的,但是浏览器是多线程的,js运行通常是在浏览器中进行的,像定时器、事件监听都是交给WebAPI处理,有结果后将回调函数放入任务队列等待js去取。

而一个浏览器通常由以下几个常驻的线程:

  • 渲染引擎线程:该线程负责页面的渲染
  • JS引擎线程:负责JS的解析和执行
  • 定时触发器线程:处理定时事件,比如setTimeout, setInterval
  • 事件触发线程:处理DOM事件
  • 异步http请求线程:处理http请求

虽然JavaScript是单线程的,可是浏览器内部不是单线程的。一些I/O操作、定时器的计时和事件监听(click, keydown…)等都是由浏览器提供的其他线程来完成的。

如何查找一个对象数组

使用find

find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。

find() 方法为数组中的每个元素都调用一次函数执行:

  • 当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。
  • 如果没有符合条件的元素返回 undefined

使用filter

Promise解决了什么问题?存在什么问题和优化?是最终解决方案吗?

解决了回调地域的问题。

问题:

1、Promise内部产生的错误或异常,如果我们不加catch方法,那么这些错误就会被吞掉,后期就不好调试。

2、Promise可以链式调用,是符合我们思考逻辑的,但是脑子还是得额外处理then,处理还是没有完全同步化,目前最优的方法是使用async函数。

async函数

async 函数是 Generator 函数的语法糖,async不再需要执行器,执行async函数,就会自动执行函数内部的逻辑。await后面既可以是promise也可以是任意类型的值,此时等同于同步操作。

async 函数的返回值是 Promise 对象,可以使用then方法添加回调函数

async 和 await原理。

async 函数本质上是generator函数的语法糖,内部有自动的执行器,不用我们手动执行next方法。

假设async函数是个方法,await 类比于 yield,设计思路:

1、async函数接受一个generator函数作为参数,返回一个函数,函数内部生成生成器,返回Promise

2、在 Promise 中有个自动执行的方法 step,step方法负责执行生成器,根据获取到的结果中的done,判断是否执行完成,执行完成,resolve修改Promise状态;没有完成,则等待当前的value执行完成,即Promise改变状态后,再执行step进行自动执行。

3、执行Step

const asyncToGenerator = (generator) => {
  return function () {
    let gen = generator.apply(this, arguments)

    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result

        try {
          result = gen[key](arg)
        } catch (error) {
          reject(error)
        }

        const { value, done } = result

        if (done) {
          return resolve(value)
        } else {
          //等待value执行完毕
          return Promise.resolve(value).then(
            (v) => {
              step('next', v)
            },
            (e) => {
              step('throw', e)
            }
          )
        }
      }

      step('next')
    })
  }
}

//测试
const getData = () =>
  new Promise((resolve) => setTimeout(() => resolve('data'), 1000))

//async函数调用
async function test() {
  const data = await getData()
  console.log('data: ', data)
  const data2 = await getData()
  console.log('data2: ', data2)
  return 'success'
}

test().then((res) => console.log(res))

//使用我们的generator创建的async函数调用
const test = asyncToGenerator(function* testG() {
  const data = yield getData()
  console.log('data: ', data)
  const data2 = yield getData()
  console.log('data2: ', data2)
  return 'success'
})
test().then((res) => console.log(res))
Promise,async awit的区别

都是用来解决异步编程的方法,Promise是使用链式调用,而aysnc更为简洁。

Typescript 和 JavaScript的区别是什么

1、Javascript是一个弱类型语言,Typescript他是提供了类型系统,会进行静态检查,如果发现有错误,编译的时候就会报错。

2、TypeScript 中引入了模块的概念,可以把声明、数据、函数和类封装在模块中。

require 和 import 引入依赖的区别
  • require 全部引入,Import可以部分引入
  • require将文件拷贝过来,Import引用定义
  • require运行时加载,import编译时加载
函数柯里化

函数柯里化是指将一次性接受参数的函数变为可以多次接受参数,也就是内部返回接收参数的函数。

用处:
参数复用:公共的参数已经通过柯里化预置了。
例如,我们在一个页面即要校验多个邮箱,又要校验多个手机号码,这时候我们封装了一个校验的方法,接受正则表达式和字符串,我们需要校验多个的时候,需要重复的传入正则表达式,很冗余。
那么这个时候我们就可以通过柯里化,将公共的参数 正则表达式预置了,调用柯里化函数进行校验。

通用柯里化

注:

函数名.length表示的是函数定义的参数的个数。

arguments.length指的是外部调用时传入的形参的个数

//...args 预置公共参数
var curry = function (fn, ...args) {
  return _curry.call(this, fn, fn.length, ...args)
}

function _curry(fn, len, ...args) {
  return function (...params) {
    let allArgs = [...args, ...params]
    if (allArgs.length >= len) {
      return fn.apply(this, allArgs)
    } else {
      return _curry.call(this, fn, len, ...allArgs)
    }
  }
}

//test1
function isNum(reg, str) {
  return reg.test(str)
}

const checkNum = /^[\d]{3}$/
const curryNum = curry(isNum, checkNum)

console.log(curryNum('123'))
console.log(curryNum('123s'))
console.log(curryNum('1s23'))
console.log(curryNum('3'))

//test2
var multify = function (a, b, c) {
  return a * b * c;
}
var multi = curry(multify);
console.log(multi(1)(2, 3));//6

//固定长度
function Curry(fn, ...args) {
  let len = fn.length
  //该函数返回一个新函数接受参数
  function _curry(fn, len, ...fnargs) {
    return function (...newargs) {
      let allargs = [...fnargs, ...newargs]
      if (allargs.length === len) {
        //执行
        return fn.apply(this, allargs)
      } else {
        //返回新函数 接受参数
        return _curry.call(this, fn, len, ...allargs)
      }
    }
  }
  return _curry.call(this, fn, len, ...args)
}

let multify = function (a, b, c) {
  return a * b * c
}
let mul = Curry(multify)
console.log(mul(2)(3)(4))

**缺点:**需要提前知道参数长度

如果要实现一个加法函数,但是不知道传进来的参数有多少,你会怎么做?

使用函数柯里化

function addCurry(...args) {
  let fn = function (...params) {
    let fnArgs = [...args, ...params]
    return addCurry.apply(this, fnArgs)
  }
  fn.toString = function () {
    return args.reduce((sum, num) => {
      return (sum += num)
    })
  }
  return fn
}

console.log(addCurry(1)(2)(3)(4)(5, 5).toString())

正则表达式
实现一个红绿灯系统,红色2s,黄灯1s,绿灯3s,promise,循环
function red() {
  console.log('red');
}

function green() {
  console.log('green');
}

function yellow() {
  console.log('yellow');
}

var createLight = function (fn, time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      fn();
      resolve();
    }, time);
  })
}

const run = function () {
  Promise.resolve().then(() => {
    return createLight(red, 2000);
  }).then(() => {
    return createLight(yellow, 1000);
  }).then(() => {
    return createLight(green, 3000);
  }).then(() => {
    return run();
  })
}
run()
数组去重的方法
const arr = [1, 1, 1, 1, 2, 2, 2, 3]

针对以上Arr去重
1、new Set()

console.log(Array.from(new Set(arr)))

2、使用sort排序后,遍历一遍,与前一元素相同则删除

function unique(arr) {
  arr.sort((a, b) => a - b)
  for (let i = 0; i < arr.length; i++) {
    if (i && arr[i] === arr[i - 1]) {
      arr.splice(i, 1)
      i--
    }
  }
  return arr
}

console.log(unique(arr))

3、2层for循环,splice删除元素

function unique(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      //相同 删除
      if (arr[i] === arr[j]) {
        arr.splice(j, 1)
	//为什么要-1 因为删除元素后,下一个要拿取的元素就到了当前位置,如果不-1就会跳过。
        j--
      }
    }
  }
  return arr
}

console.log(unique(arr))

4、创建临时空间,使用indexOf去重

function unique(arr) {
  const res = []
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) {
      res.push(arr[i])
    }
  }
  return res
}

console.log(unique(arr))

5、创建临时空间,使用includes去重
6、使用map去重

let map = new Map()
for (let i = 0; i < arr.length; i++) {
  if (!map.has(arr[i])) {
    map.set(arr[i], 1)
  }
}
console.log(Array.from(map).map((item) => item[0]))
BOM 和 DOM的区别
  • DOM是文档对象模型,BOM是浏览器对象模型。

  • DOM是将页面抽象成了节点,节点组成DOM树。

  • BOM提供的API,用于支持访问和操作浏览器的窗口。

引入脚本的方式有哪些

1、行内脚本

2、内部js脚本,可以写async或defer属性。

3、外部脚本,引入脚本文件。

4、动态加载脚本

DOMcontentload 和 load的区别
  • DomcontentLoad:HTML文档加载完毕后,不等待其他资源加载,触发。
  • Load:所有资源加载完毕后触发。
介绍一下懒加载和预加载。图片懒加载和图片预加载的区别?分别在什么场景下使用?

图片懒加载

核心逻辑:判断当前图片是否到了可视区域。

let imgs = document.querySelectorAll('img[data-src][lazyload]')
let viewHeght = document.documentElement.clientHeight

function LazyLoad() {
  //遍历图片
  for (let img of imgs) {
    //判断是否在可视区
    let imgRect = img.getBoundingClientRect()
    if (imgRect.bottom > 0 ** imgRect.top < viewHeght) {
      let image = new Image()
      image.onload = function () {
        img.src = img.dataset['src']
        img.removeAttribute('data-src')
        img.removeAttribute('lazyload')
      }
    }
  }
}

LazyLoad()

window.addEventListener('scroll', LazyLoad)

图片预加载

采用异步的方式的方式加载图片,在后面偷偷加载。

  • 为什么使用懒加载
  • 懒加载的原理
  • 为什么使用预加载
  • 实现预加载的方法
  • 预加载的应用场景
  • 预加载器的原理
为什么使用懒加载

1、优化用户体验,如果页面上所有图片都要加载并且数量很大,就需要等待很久,对用户体验不好。

2、减少服务器的压力。

3、防止并发加载图片资源阻塞js的加载。

懒加载的原理

核心逻辑:判断当前图片是否到了可视区域。

1、页面上的图片的 src 属性设为空字符串,而图片的真实路径则设置在data-src属性中

2、页面滚动,需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域

3、如果图片在可视区内将图片的 src 属性设置为data-original 的值,这样就可以实现延迟加载。

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Lazyload</title>
    <style>
      .image-item {
	    display: block;
	    margin-bottom: 50px;
	    height: 200px;//一定记得设置图片高度
	}
    </style>
</head>
<body>
  <img src="" class="image-item" lazyload="true"  data-original="images/1.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/2.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/3.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/4.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/5.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/6.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/7.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/8.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/9.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/10.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/11.png"/>
  <img src="" class="image-item" lazyload="true"  data-original="images/12.png"/>
  <script>
    var viewHeight =document.documentElement.clientHeight//获取可视区高度
    function lazyload(){
   	 var eles=document.querySelectorAll('img[data-original][lazyload]'Array.prototype.forEach.call(eles,function(item,index){
    	var rect
    	if(item.dataset.original==="") return
   	  rect=item.getBoundingClientRect()// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
    	if(rect.bottom>=0 && rect.top < viewHeight){
        !function(){
          var img=new Image()
          img.src=item.dataset.original
          img.onload=function(){
            item.src=img.src
            }
          item.removeAttribute("data-original"//移除属性,下次不再遍历
          item.removeAttribute("lazyload"}()
      }
     })
    }
    lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
    document.addEventListener("scroll",lazyload)
  </script>
  </body>
</html>

如何判断元素是否进入可视区域

1、监听父级对象的scroll事件,触发图片load

2、通过getBoundingClientRect() 或者Intersection Observer 新API判断元素的可见性。

为什么使用预加载

在页面全部加载完毕之前,对一些主要内容进行加载,先让用户看到,减少用户的等待时间。

有一些资源我们希望浏览器能尽早发现,防止重新渲染。

实现预加载的方法

link标签的prefetch和preload。

prefetch :利用浏览器空闲时间来下载未来可能访问的资源。(只获取不处理)

preload :下载未来一定需要的访问的资源。(获取后放到内存中,遇到就执行)

预加载的应用场景

字体提前加载。提升视觉稳定性,尽可能避免字体闪烁。

<link rel=preload href='font.woff2' as=font type='font/woff2' crossorigin />
JS垃圾回收机制说一下

JS的垃圾回收机制是,垃圾回收程序会定期找出那些不再继续使用的变量,然后释放其内存。

垃圾回收实现方式主要有两种,标记清除引用计数

标记清除:

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

引用计数:

引用计数是指每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

如果页面卡顿,你觉得可能是什么原因造成的?有什么办法锁定原因并解决吗?

1、network看请求时间,是不是返数据太慢。(文件体积大,bundle拆分)

2、使用Performance拍照,查看是哪个方法时间长,导致了什么问题

请描述一下 cookies,sessionStorage 和 localStorage 的区别

浏览器端常用的存储技术是 cookie 、localStorage 和 sessionStorage。

cookie 其实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据。

localStorage 、sessionStorage 是 html5 提供的浏览器本地存储的方法,均能存储5M左右数据。

  • localStorage:除非手动删除,否则不会失效。
  • sessionStorage:关闭当前窗口则失效。

详细的资料可以参考: 《请描述一下 cookies,sessionStorage 和 localStorage 的区别?》 [《浏览器数据库 IndexedDB 入门教程》](

介绍一下你了解的设计模式

单例模式、策略模式、代理模式、发布-订阅模式、观察者模式

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

应用场景:Vuex中的Store、 Mobx的Store

以Mobx为例,我们一般会抽象出一个功能的Store,这个Store在全局Store里实例化,并只能通过全局Store的stores进行访问。

策略模式

不同的参数命中不同的策略。

场景:比如一个结算系统,有A、B、C三类用户,不同用户的折扣不一样,这时候,我们可以使用策略模式来获取用户的优惠后金额。

Demo:获取年终奖。

const strategy = {
  'S': function(salary) {
    return salary * 4
  },
  'A': function(salary) {
    return salary * 3
  },
  'B': function(salary) {
    return salary * 2
  }
}

const calculateBonus = function(level, salary) {
  return strategy[level](salary)
}

calculateBonus('A', 10000) // 30000

代理模式

为一个对象提供一个占位符,方便控制。

场景:图片预加载,先通过一张loading图占位,然后通过异步的方式加载图片,等图片onload加载好后,再把完成的图片加载到img标签里面。

const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()

const proxyImage = (function() {
  const img = new Image()
  img.onload = function() { // http 图片加载完毕后才会执行
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      myImage.setSrc('loading.jpg') // 本地 loading 图片
      img.src = src
    }
  }
})()

proxyImage.setSrc('http://loaded.jpg')
  • 图片懒加载原理

观察者模式

定义对象间的一对多的依赖关系,一个对象维持一系列依赖于它的Observer对象,当状态发生变更时,通知一系列 Observer 对象进行更新。

/**
 * describe: 实现一个观察者模式
 */
let data = {
    hero: '凤凰',
};
//用来储存 订阅者 的数组
let subscribers = [];
//订阅 添加订阅者 方法
const addSubscriber = function(fn) {
    subscribers.push(fn)
}
//发布
const deliver = function(name) {
    data.hero = name;
    //当数据发生改变,调用(通知)所有方法(订阅者)
    for(let i = 0; i<subscribers.length; i++){
        const fn = subscribers[i]
        fn()
    }
}
//通过 addSubscriber 发起订阅
addSubscriber(() => {
    console.log(data.hero)
})
//改变data,就会自动打印名称
deliver('发条') 
deliver('狐狸')
deliver('卡牌')

发布-订阅模式

发布-订阅模式是指:

发布对象通过发布主题事件通知订阅该主题的对象。

订阅对象通过自定义事件订阅多个主题

例子:Event Bus就是采用的发布订阅模式实现的。

页面性能优化
1、代码压缩层面
2、关键资源
3、浏览器缓存
4CDN
5、图片优化
6、防抖节流
7、减少回流与重绘

1、代码压缩:css、js代码代码压缩

2、Gzip压缩/Br压缩

3、推迟非关键资源的加载:异步加载的三种方式——async和defer、动态脚本创建

  • async属性

    • 在script上边使用async属性,会异步执行引入的JavaScript。
    • 如果是多个脚本,该方法不能保证脚本按顺序执行。
  • defer属性

    • 在script上边使用defer属性,会延迟执行引入的JavaScript。
    • 若果是多个脚本,可以确保设置defer属性的脚本按顺序执行。
  • 动态创建script标签

    • window.onload方法确保页面加载完毕再将script标签插入到DOM中

4、利用浏览器缓存,减少服务器压力。

5、使用CDN

6、图片优化:懒加载、预加载。

7、针对一些行为进行节流和防抖,防止频繁向服务器发请求。

8、减少回流和重绘。

9、路由懒加载

  • async 和 defer 的作用是什么?有什么区别?
  • 介绍一下浏览器缓存
  • 强缓存是如何实现的
  • 协商缓存是如何实现的
  • 介绍一下CDN及其基本原理
  • 介绍一下CDN回源策略
  • 介绍一下节流和防抖
  • 介绍一下懒加载的原理
  • 介绍一下预加载的原理
  • 介绍一下回流和重绘
  • 如何减少回流和重绘
首屏渲染优化方式

1、推迟非关键资源的加载:异步加载的三种方式——async和defer、动态脚本创建

2、路由懒加载

3、CDN

4、压缩

5、浏览器缓存

介绍一下预加载的原理

先通过一张loading图占位,然后通过异步的方式加载图片,等图片onload加载好后,再把完成的图片加载到img标签里面。

实现div拖动效果
div {
    position: absolute;
    width: 100px;
    height: 100px;
    background-color: red;
    margin: auto;
}
<div></div>
<script>
    let box = document.querySelector('div')
box.onmousedown = function (e) {
    //获取当前坐标
    let x_down = e.clientX
    let y_down = e.clientY
    let left = this.offsetLeft
    let top = this.offsetTop
    console.log('down')
    box.onmousemove = function (e) {
        //获取到移动的位置
        let x_move = e.clientX
        let y_move = e.clientY
        let movex = x_move - x_down
        let movey = y_move - y_down
        console.log('move')
        this.style.left = left + movex + 'px'
        this.style.top = top + movey + 'px'
    }

    box.onmouseup = function (e) {
        //清除监听
        this.onmousemove = null
    }
}
</script>
js和其他语言的区别
  1. JS 是弱类型语言,其他语言基本是强类型语言
  2. JavaScript 对象是基于原型的,其他语言基本是基于类的
  3. JS是单线程的,其他语言基本上是多线程的。
为什么 Js 要设计成弱类型语言

优点:使用简单、灵活,可以专注于逻辑,更少的关心语法。

缺点:没有类型检查,容易出错,不好排查。

为什么js是单线程但是有settimout

js是单线程的,但是浏览器是多线程的,浏览器有很多线程:

  • js引擎线程
  • 渲染引擎线程
  • 定时器线程
  • 事件线程
  • 请求线程等等

像SetTimeout,遇到的时候,会将其交给浏览器线程进行处理,处理完了之后,将回调放入任务队列中,执行栈执行完过来取。

JS中的数组与其他语言的数组的区别

在 C 或者 Java 等其他语言中,数组大小是固定的且内存地址是连续的,不会进行动态扩容。

而在 JS 中,数组就是对象,JS数组的元素在内存中并不一定是相邻的。

红宝书的目录结构说说

红宝书的目录结构是一个循序渐进的过程,首先他是先介绍一些基础,比如基础数据类型、引用数据类型,然后又围绕这些展开,讲对象、类、面向对象等等。

然后又继续深入的学习ES6的一些内容,Promise、代理等等。

再往后就是介绍一下请求、浏览器存储等等。

对移动端开发有什么了解

常用的移动端适配方案

1、flexble.js+rem

2、vw、wh

前端设计组件的原则

一般按照功能来进行组件的封装,原则

1、低耦合,提高复用能力。尽可能考虑到更加通用的使用场景,而不是满足特定的开发需求。

比如,数据不要写死,通过参数化配置传入;或者发送请求的API通过参数传入等。

2、统一的状态管理。可以把组件中特定功能的逻辑和数据抽离出来,用一个Store进行管理。

3、State或Props不要嵌套太深,3层以内。

4、使用封装组件的组件无感知,所有逻辑处理都在组件内。使用的组件不需要特殊处理。

具体还能结合业务进行考量。

JS 的 sort() 是怎么实现的

是使用插入和快排实现的。在某个长度内使用插入,在大于某个长度时使用快排。

对象去重
var arr = [
  { name: 'uzi', color: 'blue' },
  { name: 'pdd', color: 'white' },
  { name: 'mlxg', color: 'orange' },
  { color: 'blue', name: 'uzi' },
]

function delObj(obj) {
  var uniques = []
  var map = {}
  for (var i = 0; i < obj.length; i++) {
    //获取当前对象的key值 并排序
    var keys = Object.keys(obj[i]).sort()
    var str = ''
    //拼接键值对字符串
    for (var j = 0; j < keys.length; j++) {
      str += JSON.stringify(keys[j]) + JSON.stringify(obj[i][keys[j]])
    }
    if (!map[str]) {
      uniques.push(obj[i])
      map[str] = true
    }
  }
  return uniques
}

console.log(delObj(arr))
根据对象的id去重

filter+Map

function quchong(arr){
    let map = new Map();
    return arr.filter((item)=>{
        if(!map.has(item.id)){
            map.set(item.id,1)
            return item;
        }
    })
}

两层for循环+辅助空间

手写一下发布-订阅模式
class PubSub {
  constructor() {
    this._listner = new Map()
  }

  //注册订阅行为
  subscribe(type, fn) {
    let hanlder = this._listner.get(type)
    if (!hanlder) {
      handler = []
    }
    this._listner.set(type, [...handler, fn])
  }

  //发布时间
  publish(type, ...args) {
    let handler = this._listner.get(type)
    for (let i = 0; i < handler.length; i++) {
      handler[i].call(this, ...args)
    }
  }

  //取消订阅
  unsubscribe(type, fn) {
    let handler = this._listner.get(type)
    let position = -1
    for (let i = 0; i < handler.length; i++) {
      if (handler[i] === fn) {
        position = i
      }
    }
    if (position !== -1) {
      handler.splice(position, 1)
      this._listner.set(type, hanlder)
    }
  }
}

发布订阅模式图解

既然React/Vue可以用Event Bus进行组件通信,你可以实现下吗?

通过发布-订阅模式实现。首先发布者是要的 emit 方法发布事件,而订阅者则通过addListener 进行订阅。

class EventEmitter {
  constructor() {
    this._events = new Map() //保存type及回调。
  }
}

EventEmitter.prototype.addListener = function (type, fn) {
  //添加监听
  let hanlder = this._events.get(type)
  //如果没有handler
  if (!hanlder) {
    hanlder = []
  }
  //展开放入
  this._events.set(type, [...hanlder, fn])
}

EventEmitter.prototype.removeListener = function (type, fn) {
  const handler = this._events.get(type)
  let position = -1
  //handler为数组,得找到对应的函数
  for (let i = 0; i < handler.length; i++) {
      if (handler[i] === fn) {
          position = i
      }
  }
    if (position !== -1) {
        handler.splice(position, 1)
        //移除后只剩一个,则回归函数状态
        this._events.set(type, handler[0])
    }
}

EventEmitter.prototype.emit = function (type, ...payload) {
  //通过type拿event并触发
  let handler = this._events.get(type)
  //触发多个监听
  for (let i = 0; i < handler.length; i++) {
    handler[i].call(this, ...payload)
  }
}

let emitter = new EventEmitter()
function sayHi(name) {
  console.log(`hi ${name}`)
}

function sayHello(name) {
  console.log(`hello ${name}`)
}
emitter.addListener('sayHi', sayHi)
emitter.addListener('sayHi', sayHello)
emitter.emit('sayHi', 'ch')

介绍一下AST

AST抽象语法树,本质上就是将源代码抽象为语法结构的树,比如一个函数,可能会被抽象为id为方法名,params参数,body函数体等等。

如何从纯文本获取AST

1、词法分析,扫描scanner。按预定规则合并成标识token,放入tokens列表中。

一个个读取代码,遇到空格、操作符或特殊符号,认为一个短暂的完成。

2、语法分析,解析器。将词法分析的数组转换为树形,同时验证语法。

两个 tab 的页面之间怎么通信?

1、web scoket通信,服务器作为中间介,进行消息转发。

2、使用localStorage进行通信。一个页面修改数据,另一个页面监听localStorage变化,拿到最新数据。

如何获取嵌套对象所有的Keys

1、递归获取:如果Key对应的value是对象,继续递归获取,直到不是对象。(有点类似于实现深拷贝)

var obj = {
  name: 'ch',
  address: {
    city: 'peking',
    small: 'haidian',
  },
}
// 1、递归法
let findKeys = (obj) => {
  //递归出口
  if (typeof obj !== 'object') {
    return obj
  }
  let res = []
  //遍历obj
  for (let key in obj) {
    res.push(key)
    if (typeof obj[key] === 'object') {
      res = res.concat(findKeys(obj[key]))
    }
  }
  return res
}
console.log(findKeys(obj))//['name','address','city','small']

2、非递归获取:利用一个栈存放对象,如果是对象放入栈中,一直到不是对象 逐个取出。

var obj = {
  name: 'ch',
  address: {
    city: 'peking',
    small: 'haidian',
  },
  data: 'ss',
}

// 1、非递归法,先序遍历
const isObject = (obj) => typeof obj === 'object'
const findKeys = (obj) => {
  const stack = [],
    res = []
  let current = obj
  while (stack.length || isObject(current)) {
    //把current对象的keys全部拿出来,再将为对象的值推入stack中,current 再从stack中取
    for (let key in current) {
      res.push(key)
      if (isObject(current[key])) {
        stack.push(current[key])
      }
    }
    current = stack.pop()
  }
  return res
}

console.log(findKeys(obj))
;