前端面试题

前端面试题

说说JavaScript中的数据类型?存储上的差别?

1. 基本类型 基本类型主要为以下6种:

    Number String Boolean Undefined null symbol

2. 引用类型 复杂类型统称为Object,我们这里主要讲述下面三种:

    Object Array Function Date、RegExp、Map、Set

3. 存储区别

基本数据类型和引用数据类型存储在内存中的位置不同:

  • 基本数据类型存储在栈中

  • 引用类型的对象存储于堆中

当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值

下面来举个例子

基本类型


	let a = 10;
	let b = a; // 赋值操作
	b = 20;
	console.log(a); // 10值

a的值为一个基本类型,是存储在栈中,将a的值赋给b,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址
下图演示了基本类型赋值的过程:

在这里插入图片描述

引用类型

	var obj1 = {}
	var obj2 = obj1;
	obj2.name = "Xxx";
	console.log(obj1.name); // xxx

引用类型数据存放在内对内中,每个堆内存中有一个引用地址,该引用地址存放在栈中

obj1是一个引用类型,在赋值操作过程汇总,实际是将堆内存对象在栈内存的引用地址复制了一份给了obj2,实际上他们共同指向了同一个堆内存对象,所以更改obj2会对obj1产生影响
下图演示这个引用类型赋值过程
在这里插入图片描述

小结

声明变量时不同的内存地址分配:

  • 简单类型的值存放在栈中,在栈中存放的是对应的值
    引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
  • 不同的类型数据导致赋值变量时的不同:
    简单类型赋值,是生成相同的值,两个对象对应不同的地址
    复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象

面试官:深拷贝浅拷贝的区别?如何实现一个深拷贝?

在这里插入图片描述

一、数据类型存储

  • 基本类型
  • 引用类型

基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

二、浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
下面简单实现一个浅拷贝

function shallowClone(obj) {
    const newObj = {};
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop];
        }
    }
    return newObj;
}

JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(),== Array.prototype.concat()==
  • 使用拓展运算符实现的复制

Object.assign

var obj = {
    age: 18,
    nature: ['smart', 'good'],
    names: {
        name1: 'fx',
        name2: 'xka'
    },
    love: function () {
        console.log('fx is a great girl')
    }
}
var newObj = Object.assign({}, fxObj);

slice()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

concat()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

拓展运算符

const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

三、深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

_.cloneDeep()

const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

jQuery.extend()

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

const obj2=JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefinedsymbol函数

const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4:  Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

循环递归

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

四、区别

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别
在这里插入图片描述
从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

// 浅拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存

console.log('obj1',obj1) // obj1 { name: 'init',  arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存

console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

小结

前提为拷贝类型为引用类型的情况下:

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

面试官:什么是防抖和节流?有什么区别?如何实现?

一、是什么

本质上是优化高频率执行代码的一种手段
如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(防抖)和debounce(节流)的方式来减少调用频率

定义

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

一个经典的比喻:
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
假设电梯有两种运行策略 debouncethrottle,超时设定为15秒,不考虑容量限制
电梯第一个人进来后,15秒后准时运送一次,这是节流
电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

代码实现

节流

完成节流可以使用时间戳与定时器的写法
使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行

function throttled1(fn, delay = 500) {
    let oldtime = Date.now()
    return function (...args) {
        let newtime = Date.now()
        if (newtime - oldtime >= delay) {
            fn.apply(null, args)
            oldtime = Date.now()
        }
    }
}

使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行

function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}

可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下

function throttled(fn, delay) {
    let timer = null
    let starttime = Date.now()
    return function () {
        let curTime = Date.now() // 当前时间
        let remaining = delay - (curTime - starttime)  // 从上一次到现在,还剩下多少多余时间
        let context = this
        let args = arguments
        clearTimeout(timer)
        if (remaining <= 0) {
            fn.apply(context, args)
            starttime = Date.now()
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}

防抖

简单版本的实现

function debounce(func, wait) {
    let timeout;

    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:

function debounce(func, wait, immediate) {

    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

二、区别

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout
    setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次
如下图所示:

在这里插入图片描述

三、应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

面试官:说说你对事件循环的理解

面试官:说说你对事件循环的理解

一、是什么

首先,JavaScript 是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环
JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout 定时函数等

同步任务与异步任务的运行流程图如下:
在这里插入图片描述

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

二、宏任务与微任务

如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:

console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

如果按照上面流程图来分析代码,我们会得到下面的执行步骤:

  • console.log(1) ,同步任务,主线程中执行
  • setTimeout() ,异步任务,放到 Event Table,0 毫秒后console.log(2) 回调推入 Event Queue
  • new Promise ,同步任务,主线程直接执行
  • .then ,异步任务,放到 Event Table
  • console.log(3),同步任务,主线程执行

所以按照分析,它的结果应该是 1 => ‘new Promise’ => 3 => 2 => ‘then’
但是实际结果是:1=>‘new Promise’=> 3 => ‘then’ => 2
出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取
例子中 setTimeout回调事件是先进入队列中的,按理说应该先于 .then 中的执行,但是结果却偏偏相反
原因在于异步任务还可以细分为微任务与宏任务

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

这时候,事件循环,宏任务,微任务的关系如图所示
在这里插入图片描述

按照这个流程,它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

回到上面的题目

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)

流程如下

// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

三、async与await

async是异步的意思,await 则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await 是用来等待异步方法执行

async

async函数返回一个promise对象,下面两种方法是等效的

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

async function f(){
    // 等同于
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码
所以上述输出结果为:1fn232

四、流程分析

通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解
这里直接上代码:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析过程:

  1. 执行整段代码,遇到 console.log(‘script start’) 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行== async2==,打印
    async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await 下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

所以最后的结果是:script startasync1 startasync2promise1script endasync1 endpromise2settimeout

REACT

React 中 keys 的作用是什么?

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系,因此我们绝不可忽视转换函数中 Key 的重要性。

调用 setState 之后发生了什么?

在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

react 生命周期函数

  • 初始化阶段:
    • getDefaultProps:获取实例的默认属性
    • getInitialState:获取每个实例的初始化状态
    • componentWillMount:组件即将被装载、渲染到页面上
    • render:组件在这里生成虚拟的 DOM 节点
    • componentDidMount:组件真正在被装载之后
  • 运行中状态:
    • componentWillReceiveProps:组件将要接收到属性的时候调用
    • shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)
    • componentWillUpdate:组件即将更新不能修改属性和状态
    • render:组件重新描绘
    • componentDidUpdate:组件已经更新
  • 销毁阶段:
    • componentWillUnmount:组件即将销毁

shouldComponentUpdate 是做什么的,(react 性能优化是哪个周期函数?)

shouldComponentUpdate 这个方法用来判断是否需要调用 render 方法重新描绘 dom。因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。

参考react 性能优化-sf

为什么虚拟 dom 会提高性能?(必考)

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。

用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

react diff 原理(常考,大厂必考)

  • 把树形结构按照层级分解,只比较同级元素。
  • 给列表结构的每个单元添加唯一的 key 属性,方便比较。
  • React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
  • 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
  • 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。

React 中 refs 的作用是什么?

Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

展示组件(Presentational component)和容器组件(Container component)之间有何不同

  • 展示组件关心组件看起来是什么。展示专门通过 props 接受数据和回调,并且几乎不会有自身的状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态。
  • 容器组件则更关心组件是如何运作的。容器组件会为展示组件或者其它容器组件提供数据和行为(behavior),它们会调用 Flux actions,并将其作为回调提供给展示组件。容器组件经常是有状态的,因为它们是(其它组件的)数据源。

类组件(Class component)和函数式组件(Functional component)之间有何不同

  • 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态
  • 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 ‘无状态组件(stateless component)’,可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件

(组件的)状态(state)和属性(props)之间有何不同

  • State 是一种数据结构,用于组件挂载时所需数据的默认值。State 可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果。
  • Props(properties 的简写)则是组件的配置。props 由父组件传递给子组件,并且就子组件而言,props 是不可变的(immutable)。组件不能改变自身的 props,但是可以把其子组件的 props 放在一起(统一管理)。Props 也不仅仅是数据–回调函数也可以通过 props 传递。

何为受控组件(controlled component)

在 HTML 中,类似 , 和 `` 这样的表单元素会维护自身的状态,并基于用户的输入来更新。当用户提交表单时,前面提到的元素的值将随表单一起被发送。但在 React 中会有些不同,包含表单元素的组件将会在 state 中追踪输入的值,并且每次调用回调函数时,如 onChange 会更新 state,重新渲染组件。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为"受控元素"。

何为高阶组件(higher order component)

高阶组件是一个以组件为参数并返回一个新组件的函数。HOC 运行你重用代码、逻辑和引导抽象。最常见的可能是 Redux 的 connect 函数。除了简单分享工具库和简单的组合,HOC 最好的方式是共享 React 组件之间的行为。如果你发现你在不同的地方写了大量代码来做同一件事时,就应该考虑将代码重构为可重用的 HOC。

为什么建议传递给 setState 的参数是一个 callback 而不是一个对象

因为 this.props 和 this.state 的更新可能是异步的,不能依赖它们的值去计算下一个 state。

应该在 React 组件的何处发起 Ajax 请求

在 React 组件中,应该在 componentDidMount 中发起网络请求。这个方法会在组件第一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。更重要的是,你不能保证在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState,这将不起作用。在 componentDidMount 中发起网络请求将保证这有一个组件可以更新了。

描述事件在 React 中的处理方式。

为了解决跨浏览器兼容性问题,您的 React 中的事件处理程序将传递 SyntheticEvent 的实例,它是 React 的浏览器本机事件的跨浏览器包装器。

这些 SyntheticEvent 与您习惯的原生事件具有相同的接口,除了它们在所有浏览器中都兼容。有趣的是,React 实际上并没有将事件附加到子节点本身。React 将使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的,这也意味着在更新 DOM 时,React 不需要担心跟踪事件监听器。

createElement 和 cloneElement 有什么区别?

React.createElement():JSX 语法就是用 React.createElement()来构建 React 元素的。它接受三个参数,第一个参数可以是一个标签名。如 div、span,或者 React 组件。第二个参数为传入的属性。第三个以及之后的参数,皆作为组件的子组件。

React.createElement(
    type,
    [props],
    [...children]
)

React.cloneElement()与 React.createElement()相似,不同的是它传入的第一个参数是一个 React 元素,而不是标签名或组件。新添加的属性会并入原有的属性,传入到返回的新元素中,而就的子元素奖杯替换。

React.cloneElement(
  element,
  [props],
  [...children]
)

React 中有三种构建组件的方式

React.createClass()、ES6 class 和无状态函数。

react 组件的划分业务组件技术组件?

  • 根据组件的职责通常把组件分为 UI 组件和容器组件。
  • UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。
  • 两者通过 React-Redux 提供 connect 方法联系起来。

简述 flux 思想

Flux 的最大特点,就是数据的"单向流动"。

  1. 用户访问 View
  2. View 发出用户的 Action
  3. Dispatcher 收到 Action,要求 Store 进行相应的更新
  4. Store 更新后,发出一个"change"事件
  5. View 收到"change"事件后,更新页面

了解 redux 么,说一下 redux 把

  • redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理,主要有三个核心方法,action,store,reducer,工作流程是 view 调用 store 的 dispatch 接收 action 传入 store,reducer 进行 state 操作,view 通过 store 提供的 getState 获取最新的数据,flux 也是用来进行数据操作的,有四个组成部分 action,dispatch,view,store,工作流程是 view 发出一个 action,派发器接收 action,让 store 进行数据更新,更新完成以后 store 发出 change,view 接受 change 更新视图。Redux 和 Flux 很像。主要区别在于 Flux 有多个可以改变应用状态的 store,在 Flux 中 dispatcher 被用来传递数据到注册的回调事件,但是在 redux 中只能定义一个可更新状态的 store,redux 把 store 和 Dispatcher 合并,结构更加简单清晰
  • 新增 state,对状态的管理更加明确,通过 redux,流程更加规范了,减少手动编码量,提高了编码效率,同时缺点时当数据更新时有时候组件不需要,但是也要重新绘制,有些影响效率。一般情况下,我们在构建多交互,多数据流的复杂项目应用时才会使用它们

redux 有什么缺点

  • 一个组件所需要的数据,必须由父组件传过来,而不能像 flux 中直接从 store 取。
  • 当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新 render,可能会有效率影响,或者需要写复杂的 shouldComponentUpdate 进行判断。

基本知识

1. 区分Real DOM和Virtual DOM

Real DOMVirtual DOM
1. 更新缓慢。1. 更新更快。
2. 可以直接更新 HTML。2. 无法直接更新 HTML。
3. 如果元素更新,则创建新DOM。3. 如果元素更新,则更新 JSX 。
4. DOM操作代价很高。4. DOM 操作非常简单。
5. 消耗的内存较多。5. 很少的内存消耗。

2. 什么是React?

  • React 是 Facebook 在 2011 年开发的前端 JavaScript 库。
  • 它遵循基于组件的方法,有助于构建可重用的UI组件。
  • 它用于开发复杂和交互式的 Web 和移动 UI。
  • 尽管它仅在 2015 年开源,但有一个很大的支持社区。

3. React有什么特点?

React的主要功能如下:

  1. 它使用虚拟DOM 而不是真正的DOM。
  2. 它可以进行服务器端渲染
  3. 它遵循单向数据流或数据绑定。

4. 列出React的一些主要优点。

React的一些主要优点是:

  1. 它提高了应用的性能
  2. 可以方便地在客户端和服务器端使用
  3. 由于 JSX,代码的可读性很好
  4. React 很容易与 Meteor,Angular 等其他框架集成
  5. 使用React,编写UI测试用例变得非常容易

5. React有哪些限制?

React的限制如下:

  1. React 只是一个库,而不是一个完整的框架
  2. 它的库非常庞大,需要时间来理解
  3. 新手程序员可能很难理解
  4. 编码变得复杂,因为它使用内联模板和 JSX

7. 为什么浏览器无法读取JSX?

浏览器只能处理 JavaScript 对象,而不能读取常规 JavaScript 对象中的 JSX。所以为了使浏览器能够读取 JSX,首先,需要用像 Babel 这样的 JSX 转换器将 JSX 文件转换为 JavaScript 对象,然后再将其传给浏览器。

React 组件

8. 你怎样理解“在React中,一切都是组件”这句话。

组件是 React 应用 UI 的构建块。这些组件将整个 UI 分成小的独立并可重用的部分。每个组件彼此独立,而不会影响 UI 的其余部分。

9. 怎样解释 React 中 render() 的目的。

每个React组件强制要求必须有一个 render()。它返回一个 React 元素,是原生 DOM 组件的表示。如果需要渲染多个 HTML 元素,则必须将它们组合在一个封闭标记内,例如 、`` 等。此函数必须保持纯净,即必须每次调用时都返回相同的结果。

11. React中的状态是什么?它是如何使用的?

状态是 React 组件的核心,是数据的来源,必须尽可能简单。基本上状态是确定组件呈现和行为的对象。与props 不同,它们是可变的,并创建动态和交互式组件。可以通过 this.state() 访问它们。

12. 区分状态和 props

条件StateProps
1. 从父组件中接收初始值YesYes
2. 父组件可以改变值NoYes
3. 在组件中设置默认值YesYes
4. 在组件的内部变化YesNo
5. 设置子组件的初始值YesYes
6. 在子组件的内部更改NoYes

13. React组件生命周期的阶段是什么?

React 组件的生命周期有三个不同的阶段:

  1. *初始渲染阶段:*这是组件即将开始其生命之旅并进入 DOM 的阶段。
  2. *更新阶段:*一旦组件被添加到 DOM,它只有在 prop 或状态发生变化时才可能更新和重新渲染。这些只发生在这个阶段。
  3. *卸载阶段:*这是组件生命周期的最后阶段,组件被销毁并从 DOM 中删除。

14. 详细解释 React 组件的生命周期方法。

一些最重要的生命周期方法是:

  1. componentWillMount**()** – 在渲染之前执行,在客户端和服务器端都会执行。
  2. componentDidMount**()** – 仅在第一次渲染后在客户端执行。
  3. componentWillReceiveProps**()** – 当从父类接收到 props 并且在调用另一个渲染器之前调用。
  4. shouldComponentUpdate**()** – 根据特定条件返回 true 或 false。如果你希望更新组件,请返回true 否则返回 false。默认情况下,它返回 false。
  5. componentWillUpdate**()** – 在 DOM 中进行渲染之前调用。
  6. componentDidUpdate**()** – 在渲染发生后立即调用。
  7. componentWillUnmount**()** – 从 DOM 卸载组件后调用。用于清理内存空间。

14. React中的事件是什么?

在 React 中,事件是对鼠标悬停、鼠标单击、按键等特定操作的触发反应。处理这些事件类似于处理 DOM 元素中的事件。但是有一些语法差异,如:

  1. 用驼峰命名法对事件命名而不是仅使用小写字母。
  2. 事件作为函数而不是字符串传递。

事件参数重包含一组特定于事件的属性。每个事件类型都包含自己的属性和行为,只能通过其事件处理程序访问。

15. 你对 React 的 refs 有什么了解?

Refs 是 React 中引用的简写。它是一个有助于存储对特定的 React 元素或组件的引用的属性,它将由组件渲染配置函数返回。用于对 render() 返回的特定元素或组件的引用。当需要进行 DOM 测量或向组件添加方法时,它们会派上用场。

class ReferenceDemo extends React.Component{
     display() {
         const name = this.inputDemo.value;
         document.getElementById('disp').innerHTML = name;
     }
render() {
    return(        
          <div>
            Name: <input type="text" ref={input => this.inputDemo = input} />
            <button name="Click" onClick={this.display}>Click</button>            
            <h2>Hello <span id="disp"></span> !!!</h2>
          </div>
    );
   }
 }

16. 列出一些应该使用 Refs 的情况。

以下是应该使用 refs 的情况:

  • 需要管理焦点、选择文本或媒体播放时
  • 触发式动画
  • 与第三方 DOM 库集成

17. 你能用HOC做什么?

HOC可用于许多任务,例如:

  • 代码重用,逻辑和引导抽象
  • 渲染劫持
  • 状态抽象和控制
  • Props 控制

18. 什么是纯组件?

纯(Pure) 组件是可以编写的最简单、最快的组件。它们可以替换任何只有 render() 的组件。这些组件增强了代码的简单性和应用的性能。

React Redux

19. MVC框架的主要问题是什么?

以下是MVC框架的一些主要问题:

  • 对 DOM 操作的代价非常高
  • 程序运行缓慢且效率低下
  • 内存浪费严重
  • 由于循环依赖性,组件模型需要围绕 models 和 views 进行创建

20. 什么是Redux?

Redux 是当今最热门的前端开发库之一。它是 JavaScript 程序的可预测状态容器,用于整个应用的状态管理。使用 Redux 开发的应用易于测试,可以在不同环境中运行,并显示一致的行为。

21. Redux遵循的三个原则是什么?

  1. ***单一事实来源:***整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。
  2. ***状态是只读的:***改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
  3. ***使用纯函数进行更改:***为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpPvch8k-1645437902707)(https://segmentfault.com/img/bVbqdU5?w=515&h=485)]

22. 你对“单一事实来源”有什么理解?

Redux 使用 “Store” 将程序的整个状态存储在同一个地方。因此所有组件的状态都存储在 Store 中,并且它们从 Store 本身接收更新。单一状态树可以更容易地跟踪随时间的变化,并调试或检查程序。

23. 列出 Redux 的组件。

Redux 由以下组件组成:

  1. Action – 这是一个用来描述发生了什么事情的对象。
  2. Reducer – 这是一个确定状态将如何变化的地方。
  3. Store – 整个程序的状态/对象树保存在Store中。
  4. View – 只显示 Store 提供的数据。

24. 数据如何通过 Redux 流动?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ol3dBleU-1645437902707)(https://segmentfault.com/img/bVbqdVh?w=1292&h=560)]

25. 如何在 Redux 中定义 Action?

React 中的 Action 必须具有 type 属性,该属性指示正在执行的 ACTION 的类型。必须将它们定义为字符串常量,并且还可以向其添加更多的属性。在 Redux 中,action 被名为 Action Creators 的函数所创建。以下是 Action 和Action Creator 的示例:

function addTodo(text) {
       return {
                type: ADD_TODO,    
                 text
    }
}

26. 解释 Reducer 的作用。

Reducers 是纯函数,它规定应用程序的状态怎样因响应 ACTION 而改变。Reducers 通过接受先前的状态和 action 来工作,然后它返回一个新的状态。它根据操作的类型确定需要执行哪种更新,然后返回新的值。如果不需要完成任务,它会返回原来的状态。

27. Store 在 Redux 中的意义是什么?

Store 是一个 JavaScript 对象,它可以保存程序的状态,并提供一些方法来访问状态、调度操作和注册侦听器。应用程序的整个状态/对象树保存在单一存储中。因此,Redux 非常简单且是可预测的。我们可以将中间件传递到 store 来处理数据,并记录改变存储状态的各种操作。所有操作都通过 reducer 返回一个新状态。

28. Redux 有哪些优点?

Redux 的优点如下:

  • 结果的可预测性 - 由于总是存在一个真实来源,即 store ,因此不存在如何将当前状态与动作和应用的其他部分同步的问题。
  • 可维护性 - 代码变得更容易维护,具有可预测的结果和严格的结构。
  • 服务器端渲染 - 你只需将服务器上创建的 store 传到客户端即可。这对初始渲染非常有用,并且可以优化应用性能,从而提供更好的用户体验。
  • 开发人员工具 - 从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。
  • 社区和生态系统 - Redux 背后有一个巨大的社区,这使得它更加迷人。一个由才华横溢的人组成的大型社区为库的改进做出了贡献,并开发了各种应用。
  • 易于测试 - Redux 的代码主要是小巧、纯粹和独立的功能。这使代码可测试且独立。
  • 组织 - Redux 准确地说明了代码的组织方式,这使得代码在团队使用时更加一致和简单。

React 路由

29. 什么是React 路由?

React 路由是一个构建在 React 之上的强大的路由库,它有助于向应用程序添加新的屏幕和流。这使 URL 与网页上显示的数据保持同步。它负责维护标准化的结构和行为,并用于开发单页 Web 应用。 React 路由有一个简单的API。

30. 为什么React Router v4中使用 switch 关键字 ?

虽然 ** 用于封装 Router 中的多个路由,当你想要仅显示要在多个定义的路线中呈现的单个路线时,可以使用 “switch” 关键字。使用时,** 标记会按顺序将已定义的 URL 与已定义的路由进行匹配。找到第一个匹配项后,它将渲染指定的路径。从而绕过其它路线。

31. 列出 React Router 的优点。

几个优点是:

  1. 就像 React 基于组件一样,在 React Router v4 中,API 是 ‘All About Components’。可以将 Router 可视化为单个根组件(**),其中我们将特定的子路由(**)包起来。
  2. 无需手动设置历史值:在 React Router v4 中,我们要做的就是将路由包装在 `` 组件中。
  3. 包是分开的:共有三个包,分别用于 Web、Native 和 Core。这使我们应用更加紧凑。基于类似的编码风格很容易进行切换。

1、使用css水平垂直居中有几种实现方法?

  • 已知高度可以使用 line-height 等于 高度实现垂直居中;使用 text-align:center实现水平居中
  • display:flex; align-items:center;justify-content:center;
  • 绝对定位的话,给父元素 设置定位属性 relative,子元素设置 absolute,然后设置 子元素 top:0;left:0;right:0;bottom:0;margin:auto;

2、flex布局

flex布局即为弹性布局,也就是弹性盒模型,给元素开启弹性盒之后,子元素的float、clear、 vertical-align等失效

flex-direction:决定主轴方向

  • row(默认值):主轴为水平,起点在左端
  • row-reverse:主轴为水平,起点在右端
  • column:主轴为垂直方向,起点在上沿
  • column-reverse:起点在下沿

flex-wrap:是否换行

  • nowrap(默认) 不换行
  • wrap 换行 首行在上
  • wrap-reverse 换行 首行在下

justify-content:子元素在主轴上的对齐方式

  • flex-start(默认):左对齐
  • flex-end:右对齐
  • center:居中
  • space-between:两端对齐,子元素之间的间隔都相等
  • space-around:两端对齐(但子元素不与父元素边框贴合),子元素两侧的间隔相等;故子元素之间的间隔比子元素与父元素边框的间隔大一倍

align-item:子元素在交叉轴上对齐方式

  • flex-start:交叉轴的起点对齐
  • flex-end:交叉轴的终点对齐
  • center:中点对齐
  • baseline:子元素的第一行
  • stretch(默认):若项目为设置高度或设置为auto,将占满整个父元素

设置在子元素的属性

  • order:定义子元素的排列顺序,数值越小,排列越靠前,默认为0
  • flex-grow:定义子元素的放大比例,默认为0,即如果存在剩余空间,也不放大;如果所有子元素的flex-grow属性为1,则它们将等分剩余空间
  • flex-shrink:定义子元素的缩小比例,默认为1,如果空间不足,该子元素将缩小;如果所有子元素的flex-shrink属性都为1,当空间不足时,都将等比例缩小
  • flex-basis:定义了在分配多余空间之前,子元素占据的主轴空间,默认值为auto
  • flex:前三者的缩写,默认值为 0 1 auto。

3. rem的理解 移动端设计稿上的固定尺寸如何转化为实际的rem值

rem是以html里的font-size为基准值的长度单位,一般用于移动端适配

px、em和rem的区别

  • em是相对父元素的字体大小,如果父元素的字体大小是14px,那么它子元素的2em就是28px,不同父元素的子元素的2em的实际大小是可能不同的。

  • rem是相对于根元素,即html元素,如果html的字体大小是14px,那么在任何地方的2rem都是28px。

  • px像素(Pixel),相对长度单位。像素px是相对于显示器屏幕分辨率而言的。

为什么要用rem?

rem的出现及使用多用于移动端开发中,我们知道,移动端的设备宽度是不定的,如果我们使用固定的大小,那么在不同大小的设备上就会出现布局错乱、留白、残缺等现象的出现

动态计算

因为设备的大小我们是无法预知的,所以1rem的大小在不同设备上也就不同,如果我们在加载时知道了设备的宽度,我们就可以根据这个宽度来动态的计算出在该设备上1rem究竟应该是多少,然后设置到html元素上。

假设设计图宽度为designWidth,实际设备宽度为windowWidth,那么可以计算出实际的1rem = (designWidth/windowWidth)*100。这里的100为我们在设计图中设置的1rem的大小,也叫基准值

4. this指向

  • 普通函数调用,此时this指向window
  • 构造函数调用,this指向实例对象
  • 对象方法调用,this指向该方法所属对象
  • 事件绑定时,this指向绑定事件的对象
  • 定时器函数,this指向window
  • 箭头函数,this指向上下文

更改this指向的三个方法

  1. call()方法
  2. apply()方法
  3. bind()方法

三者区别

  • bind 会有一个返回值,返回值是个函数,因此要加上()才能调用;call,apply是没有返回值,当改变函数this指向的时候,不需要加()就会执行
  • call 传递参数的时候是一个一个传递的, apply是传递一个数组

5. 搜索框输入时频繁 发出请求怎么处理?

使用防抖处理

防抖实现原理:

如果在500ms内频繁操作,则每次都会清除一次定时器然后重新创建一个。直到最后一次操作,然后等待500ms后发送ajax。

实现方式:

  1. 防抖函数主要利用了闭包、高阶函数、定时器等特性
  2. 首先我们可以定义一个高阶函数debounce,接受一个回调函数和延迟时间,在函数内部定义一个定时器变量,用于记录当前的定时器
  3. debounce内部我们返回一个函数,函数执行的时候会检查当前是否有定时器,有的话会清除当前的定时器,重新赋值一个新的定时器给定时器变量,并设置定时器执行时间为用户传入的第二个参数
  4. 定时器内部通过apply调用用户传入的函数,并传入执行上下文和arguments
  5. 这样就能保证在规定时间内,不会高频的触发回调函数

节流

事件触发之后,在规定时间内,事件处理函数不能被再次调用,也就是说,在规定时间内,事件处理函数只能被调用一次,且是最先被触发调用的那次. 主要使用场景是滚动加载更多、搜索框的搜索联想功能

闭包引起的内存泄漏:

  • 原因:闭包可以维持函数内局部变量,使其得不到释放
  • 解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用

6. ES6新特性

看es6常见题

数组方法

1. shift 删除数组中的第一个元素
2. pop 删除数组中的最后一个元素
3. unshift 增加元素在数组的前面
4. push 增加元素在数组的后面
5. map 循环,并返回新的数组
6. forEach 循环遍历
7. filter 过滤,筛选出数组中满足条件的,并且返回新的数组
8. contact 合并数组
9. find 查找出第一个符合条件中的数组元素
10. findIndex查找出第一个符合条件中的数组元素
11. flat 将多维数组转为一维数组
12. join 将数组转为字符串
13. reverse 颠倒数组中的顺序
14. every 检测数组中元素是否都符合条件,返回值为--boolean
15. some 检测数组中元素是否有满足条件的元素,返回值为---boolean
16. splice(start,n,添加元素) 开始位置,删除个数,添加元素
17. sort 排序
18. slice(start,end)选中[start,end)之间的元素
19. indexOf 查找值所在的位置
20. includes 查看数组中是否存在此元素

7. react hooks的理解

  • 解决了什么问题?

    它可以让你在不编写class的情况下使用state以及其他的React特性,

    Hook 使你在无需修改组件结构的情况下复用状态逻辑

  • 如何用hooks实现willunmount的效果

  • useState是react自带的一个hook函数,它的作用就是用来声明状态变量。useState这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]项是当前当前的状态值,第[1]项是可以改变状态值的方法函数。

  • useEffect

两个参数

1. 第一个参数是一个函数,是在第一次渲染以及之后更新渲染之后会进行的副作用。

这个函数可能会有返回值,倘若有返回值,返回值也必须是一个函数,会在组件被销毁时执行。

2. 第二个参数是可选的,是一个数组,数组中存放的是第一个函数中使用的某些副作用属性。用来优化 useEffect

如果使用此优化,请确保该数组包含外部作用域中随时间变化且 effect 使用的任何值。 否则,您的代码将引用先前渲染中的旧值。
如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。 这告诉React你的 effect 不依赖于来自 props 或 state 的任何值,所以它永远不需要重新运行。

整合了类组件componentDidMount、componentDidUpdate、componentWillUnMount等钩子函数的能力,而且代码显得更加简洁.

虚拟DOM原理

react 在内存中生成维护一个跟真实DOM一样的虚拟DOM 树,在改动完组件后,会再生成一个新得DOM,react 会把新虚拟DOM 跟原虚拟DOM 进行比对,找到两个DOM不同的地方,然后将之统一更新到真实DOM节点上

-优点:提高渲染速度

-缺点:由于多了一层虚拟DOM计算,就会比html渲染慢

了解 redux 么,说一下 redux吧,

  • redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理,主要有三个核心方法,action,store,reducer,工作流程是 view 调用 store 的 dispatch 接收 action 传入 store,reducer 进行 state 操作,view 通过 store 提供的 getState 获取最新的数据。
  • 新增 state,对状态的管理更加明确,通过 redux,流程更加规范了,减少手动编码量,提高了编码效率,同时缺点时当数据更新时有时候组件不需要,但是也要重新绘制,有些影响效率。一般情况下,我们在构建多交互,多数据流的复杂项目应用时才会使用它们

redux解决了什么问题

redux是为了解决react组件间通信和组件间状态共享而提出的一种解决方案

  1. store:用来存储当前react状态机(state)的对象。connect后,store的改变就会驱动react的生命周期循环,从而驱动页面状态的改变

  2. action: 用于接受state的改变命令,是改变state的唯一途径和入口。一般使用时在当前组件里面调用相关的action方法,通常把和后端的通信(ajax)函数放在这里

  3. reducer: action的处理器,用于修改store中state的值,返回一个新的state值

主要解决什么问题

1、组件间通信

由于connect后,各connect组件是共享store的,所以各组件可以通过store来进行数据通信,当然这里必须遵守redux的一些规范,比如遵守 view -> aciton -> reducer的改变state的路径

2、通过对象驱动组件进入生命周期

对于一个react组件来说,只能对自己的state改变驱动自己的生命周期,或者通过外部传入的props进行驱动。通过redux,可以通过store中改变的state,来驱动组件进行update

3、方便进行数据管理和切片

redux通过对store的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo之类的操作

应该在 React 组件的何处发起 Ajax 请求

在 React 组件中,应该在 componentDidMount 中发起网络请求。这个方法会在组件第一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。更重要的是,你不能保证在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState,这将不起作用。在 componentDidMount 中发起网络请求将保证这有一个组件可以更新了

类组件(Class component)和函数式组件(Functional component)之间有何不同

  • 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态
  • 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 ‘无状态组件(stateless component)’,可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件

shouldComponentUpdate 是做什么的,(react 性能优化是哪个周期函数?)

shouldComponentUpdate 这个方法用来判断是否需要调用 render 方法重新描绘 dom。

因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能

React 性能优化的方法

在react方面的话,使用shouldcomponentupdate,purecomponent,usememo(我可以展开详谈),在浏览器方面的优化常用的有svg,减少http请求,防抖和节流也是可以减少http请求的,使用浏览器缓存(强缓存或者协商缓存),将script标签放在下面,或者是用defer和async来让它执行异步,使用http2,压缩代码,服务端渲染(可选,比较耗费服务端的性能),做cdn的静态资源托管也是可以优化性能的,使用iconfont图标来代替图片图标,用css3效果来替换那些阴影图片,减少重绘重排,使用事件委托(react的事件机制也是将事件添加给最上层的dom统一管理),降低css选择器的复杂性

项目流程

一、项目流程

说一下最近做的一个后台管理项目,该项目是一个后台管理系统,针对于心随礼动这个项目,做的一个后台管理,其中主要包括了首页,员工业绩信息展示的模块,商品分类模块,用户角色模块以及权限管理模块。 (模块的功能实现与解释写在下方)

二、项目的亮点和难点

1、用户角色的权限管理,包括用户操作数据时候的权限,输入路径可以进入的解决办法
通过与后端配合,每个角色都有一个数据,需要后台返回来,后台返回的这个数据包含了用户所拥有权限的路由的url,将这个数据遍历出来,来控制左侧菜单栏内容的显示与隐藏,不过当时在做这个的时候,后期的测试有一个弊端,就是如果用户没有某个权限,直接输入url也是可以进入的,经过和后端的沟通,通过后端返回的数据直接遍历搭建路由,而不是控制显示和隐藏,

2、登录注册的拦截,以及登录的过期时间
用户在没有登陆的时候,重定向到登录页面,通过一个admin页面统一进行重定向,在登陆的时候后端人员会给我返回一个token值,我将这个token值通过cookie的方式存入到浏览器中,默认不给cookie设置过期时间,在浏览器关闭的时候,cookie会被清除,用户下次打开浏览器的时候需要重新登陆,还有一种解决办法需要后端人员配合,后端人员通过redis来存放token的过期时间,用户每次在发送http请求时,都会在请求头中携带这个token,服务端判断token,并可以为token设置延长的过期时间

3、better-scorll实现左右联动(做移动端)
在实现商品展示页面的时候有一个左右栏联动的效果,最初尝试用原生js编写,出了很多bug,有的卡顿,有的从头开始走,之后是用到better-scorll这个插件,想要实现左右联动的功能需要两个功能,一个是手指点击左边菜单栏,右边食物栏会联动到菜单栏下面的内容,另一个是手指滑动右边食物栏,左边菜单栏会随着右侧的滚动而相应出现active样式。先实现第一个目的,我需要在左侧的目标li上绑定click事件,点击事件触发move,还需要初始化两个better-scorll对象,一个左边的meun,一个右边的food。在move函数里面执行food.scrollToElement(le,time)

这个方法简直逆天:能food里的目标元素el在time毫秒内滚动到最顶部。el可以通过move(index)来获取,实现目的2的话,也就是右侧带动左侧的联动比较复杂一点,首先我需要定义一个数组,来记录一下food中list的高度(offectheight),通过scroll事件实时监听滚动位置,并且将位置付给scrollY,scrollY变化执行回调函数来获取索引,通过索引来动态添加class

4、对用户的数据之类的使用echart展示(vue项目)
最初在使用echart的时候,在控制删除图表的时候考虑的是v-if,不过后来了解到这个v-if是通过控制删除节点,创建节点,这样对性能的消耗太大,之后了解到echart有一个clear功能,清空绘画内容,清空后实例可用,因为并非释放示例的资源,释放资源我们需要dispose(),这样算是对项目做了一项性能的优化

5、在和后端交互的时候,商讨返回数据格式

6、ant-design3和ant-design4的时候遇到的难点
在使用ant4的时候碰到了一些坑,在ant3中图标的在安装完ant插件后就可以直接调用的,但是在ant4中图标需要另外的下载,下载iconfont,以及ant4在获取数据的时候也是有一些坑的,包括获取每一条数据的那个render函数

7、自己在项目中做一些性能的优化or代码的优化
在react方面的话,使用shouldcomponentupdate,purecomponent,usememo(我可以展开详谈),在浏览器方面的优化常用的有svg,减少http请求,防抖和节流也是可以减少http请求的,使用浏览器缓存(强缓存或者协商缓存),将script标签放在下面,或者是用defer和async来让它执行异步,使用http2,压缩代码,服务端渲染(可选,比较耗费服务端的性能),做cdn的静态资源托管也是可以优化性能的,使用iconfont图标来代替图片图标,用css3效果来替换那些阴影图片,减少重绘重排,使用事件委托(react的事件机制也是将事件添加给最上层的dom统一管理),降低css选择器的复杂性

8、一级分类和二级分类
请求数据:请求分类列表的时候,一级列表的id为0,二级列表的传入参数为自身ID,根据id名来对请求道的后端数据进行判断对应的级数数据。需用到async,await(一级列表数据和二级列表数据会)再componentDidMount生命周期里调用该方法。

点击查看子分类操作:再setState里接收到一级分类的name和id,因为setState时异步操作,调用分类列表的时候需再加个

主观题

页面布局

  • 移动端页面布局适配,使用 rem(基于 html 根元素的字体大小)布局,来解决自适应问题(屏幕特别大的时候字体也会特别大),可以监听 window 的 resize 事件,动态改变 html 根元素的字体大小。
  • flex 加百分比布局(极力推荐)。

如何解决移动端点击 300 毫秒延迟?

  • 由于移动端浏览器有许多默认行为,例如双击放大,当你尝试点击一个链接的时候,浏览器要等待 300 毫秒并判断用户是要打开链接还是要执行双指事件。
    解决方案:
  1. 我们可以通过以下标签来设置视口宽度为设备宽度,以及设置是否允许用户双指缩放。
<meta name="viewport" content="width=device-width">
<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">

2.给 html 标签设置

html { touch-action: manipulation;}
  1. fastclick.js 插件,来解决点击延迟的问题。
  2. 或者使用移动端事件 toutch,tap。
  3. hammer.js,zepto.js 等前端手势库。

我现在想搞一个手机号(500 人)抽奖活动,如何实现?

  • 将这些手机号存放到数组中,取数组的随机长度的一个元素就可以实现
arr = [1,2,3,4,5];
arr.sort(() => Math.random() - 0.5);

一个字符串,我想把所有的小写 a 替换成大写的 A

let str = "addadwdasdwa";
str = str.replace(/a/g, "A");
str = str.replaceAll('a','A')

防抖和节流?

如何防止表单的重复提交?和防抖节流没有任何关系,点完提交之后设置按钮不可用就行
防抖和节流常见使用场景:

  1. 自动完成(auto complete)
  2. 滚动条事件
    在固定的较短时间间隔之内防止用户重复事件的触发

js 的垃圾回收机制?

短信验证码注册如何实现?

  1. 监听输入框的 keyup 事件,使用正则判断用户输入的是不是手机号。
  2. 如果输入的是正确的手机号码,发送按钮变为可用。
  3. 点击发送验证码之后,发 ajax 请求调用接口,把用户的手机号传到服务器端。
  4. 接口数据返回发送成功的提示信息,并且发送按钮开始倒计时。
  5. 输入手机中收到的验证码,点击注册发送请求判断验证码是否正确。

为什么 https 协议更安全?

  • HTTPS 是在 HTTP 上建立 SSL 加密层,并对传输数据进行加密,是 HTTP 协议的安全版。
  1. 对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;
  2. 对网站服务器进行真实身份认证。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFdjQzyK-1645438108511)(https://user-gold-cdn.xitu.io/2019/4/26/16a57873acced7f1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)]

网络请求数据传输时如何加密?

  • 使用 https 协议,信息会在 SSL 通道中进行加密传输,即使被人截取也无法解密。

CDN 为什么快?

  • 控制时延
  • 负载均衡

npm 和 npx 有什么区别?

npm
  • 使用 npm 安装模块时候,会先去安装第三方依赖,然后用安装好的依赖去创建项目。
npx
  • 使用 npx 安装模块的时候,会临时安装第三方依赖,再去搭建项目,当项目搭建完成时,会自动将第三方依赖删除,避免全局污染。

在图片加载不出来的时候如何设置一张默认图片

  • 用 onerror:
<img src="images/logo.png" onerror="this.src='images/errorLogo.png';">

如何删除对象中的某个属性?

  • delete obj.name

js 和 css 的顺序对前端性能的影响?

  • css 最好放在头部,这样 css 能和 dom 同时加载,如果放在尾 部或者其他地方,那就是浏览器先解析 dom 再去解析 css,会影响页面的性能。
  • js 的运行是会阻塞 dom 树的渲染的,js 有时还需要操作 dom 元素,如果 js 放在头部执行,dom 树还没有构建出来,这时是无法操作 dom 的

网页上有很多网络请求,我如何知道全部的请求都执行完毕?

  • 使用 Promise.all
  • 比如说一个页面上需要等两个或多个 ajax 的数据回来以后才正常显示,在此之前只显示 loading 图标。
  • 引申:Promise.race()
  • 返回一个执行最快 ajax 的回调

单页面应用如何进行 SEO 优化

  • 服务端渲染(SSR)
  • 预加载

封装一个函数,获取 url 中的参数列表

  • 使用 qs 插件

js 一定要知道的插件

  • lodash 数组,字符串之类的方法
  • qs 用于格式化 URL 参数的插件
  • moment,用于日期处理
  • jQuery,元素选择器

我们在做单页面应用程序的时候如何保证刷新之后数据还在?

  • 页面刷新之后重新请求数据
  • 或者将数据存储本地(localStorage)

你在做开发的时候如何实现支付功能?

  • 微信支付:后端生成一个支付签名,调用 jsAPI 来实现。
  • 支付宝支付:服务器端生成一个 url 地址,跳转就行了。
  • 混合开发 app 支付:由原生开发人员提供一个唤醒支付宝或者微信支付的方法,直接调用就 ok。

常见 git 命令?

  • git init 初始化
  • git add. 添加到暂存区
  • git commit -m 'xxx’提交
  • git pull 从仓库拉取

你平时开发的时候有没有遇到过技术难点?你是如何解决的

  • axios 网络请求的二次封装
  • http 请求传递数据格式的问题
  • 移动端开发的设备兼容问题(ios 滚动条特别卡)
  • 在 CSS 中添加-webkit-overflow-scrolling:touch

移动端的常见的手势库?

  • hammer.js
  • zepto.js

你平时喜欢逛的技术社区有哪些?

  • 掘金
  • 简书
  • 思否
  • github
  • CSDN
  • 知乎

你平时闲下来有什么爱好?

  • 玩游戏
  • 打乒乓球
  • 旅游
  • 上网冲浪

你觉得你有什么优势?

  • 首先我认为我的技术栈跟咱们公司用到的技术基本符合,我是热爱编程,比较热爱前端,乐于接受新事物。
  • 然后的话我觉得我现在比较年轻,比较有活力,学习能力和抗压能力都比较强,能加班。
  • 再有的话就是希望能在公司长期发展,跟公司一起进步。

你有什么要问我们的吗?

  • 公司当前使用的技术架构
  • 公司有没有技术培训 codereview?
  • 公司福利
  • 上下班时间

你是如何理解前后端分离这种开发模式的?

  • 前后端分离可以让大家各自做自己擅长的事情
  • 可以实现一套系统多次复用,避免重复开发。
  • 但是增加了沟通成本。
  • 前期开发的时候没有不分离的时候开发快。

后台管理如何判断权限?

{
	"user_name",
	"role": {
		id: 3,
		name: '库管'
	},
	prilleges: ['product_list', 'product_add', 'product_edit'...]
}
const routes = [{
    path: '/products/list',
    key: 'product_list',
    component: 'Product_list',
    children: [{
        path: '/products/add',
        key: 'product_add',
        component: 'Product_Add',
    },
        {
        path: '/products/edit',
        key: 'product_edit',
        component: 'Product_Edit',
    }]
}]
//
newRouts // 数组是在原有所有路由对象的基础上过滤之后的结果

//
<Menu>
      {newRouts.map(r=><Menu.item></Menu.item>)}
</Menu>

// 当你前端在调用服务器接口的时候 服务器端也会验证你的权限

// 必须记住的东西:
首先在我前端页面中我会配置好所有的路由节点,每一个节点都会添加一个自定义属性用来标识当前页面的访问权限。每一次用户登录成功之后,服务器端会返回当前用户的权限信息,然后我根据返回的权限信息动态设置我的管理系统的导航菜单

我们除了客户端验证之外还有会服务器端验证,每一次调用接口的时候服务器端都会验证用户的的权限信息,如果没有权限就会返回 401 状态码,我在页面中弹提示(全局响应拦截,根据状态码来做不同的操作)

JS

数据类型
1.面试官:JavaScript中什么是基本数据类型什么是引用数据类型?以及各个数据类型是如何存储的?

答:
基本数据类型有

Number
String
Boolean
Null
Undefined
Symbol(ES6新增数据类型)
bigInt
引用数据类型统称为Object类型,细分的话有

Object
Array
Date
Function
RegExp
基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,在栈中保存数据的引用地址,这个引用地址指向的是对应的数据,以便快速查找到堆内存中的对象。

顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null,从而减少无用内存的消耗

2.类型转换
面试官:在JS中为什么0.2+0.1>0.3?

答:

因为在JS中,浮点数是使用64位固定长度来表示的,其中的1位表示符号位,11位用来表示指数位,剩下的52位尾数位,由于只有52位表示尾数位。

而0.1转为二进制是一个无限循环数0.0001100110011001100…(1100循环)

3.面试官:判断数据类型有几种方法

答:

typeof

缺点:typeof null的值为Object,无法分辨是null还是Object
instanceof

缺点:只能判断对象是否存在于目标对象的原型链上
constructor

Object.prototype.toString.call()

一种最好的基本类型检测方式 Object.prototype.toString.call() ;它可以区分 null 、 string 、

boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。

缺点:不能细分为谁谁的实例

4.instanceof原理
instanceof原理实际上就是查找目标对象的原型链

5.面试官:为什么typeof null是Object
答:

因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object

6.面试官:=有什么区别

答:

===是严格意义上的相等,会比较两边的数据类型和值大小

数据类型不同返回false
数据类型相同,但值大小不同,返回false
==是非严格意义上的相等,

两边类型相同,比较大小

两边类型不同,根据下方表格,再进一步进行比较。

Null == Undefined ->true
String == Number ->先将String转为Number,在比较大小
Boolean == Number ->现将Boolean转为Number,在进行比较
Object == String,Number,Symbol -> Object 转化为原始类型

7.面试官:手写call、apply、bind

答:

call和apply实现思路主要是:
判断是否是函数调用,若非函数调用抛异常
通过新对象(context)来调用函数
给context创建一个fn设置为需要调用的函数
结束调用完之后删除fn
bind实现思路
判断是否是函数调用,若非函数调用抛异常
返回函数
判断函数的调用方式,是否是被new出来的
new出来的话返回空对象,但是实例的__proto__指向_this的prototype
完成函数柯里化
Array.prototype.slice.call()

call:

 Function.prototype.myCall = function (context) {
      // 先判断调用myCall是不是一个函数
      // 这里的this就是调用myCall的
      if (typeof this !== 'function') {
        throw new TypeError("Not a Function")
      }

      // 不传参数默认为window
      context = context || window

      // 保存this
      context.fn = this

      // 保存参数
      let args = Array.from(arguments).slice(1)   //Array.from 把伪数组对象转为数组

      // 调用函数
      let result = context.fn(...args)

      delete context.fn

      return result

    }

apply

Function.prototype.myApply = function (context) {
      // 判断this是不是函数
      if (typeof this !== "function") {
        throw new TypeError("Not a Function")
      }

      let result

      // 默认是window
      context = context || window

      // 保存this
      context.fn = this

      // 是否传参
      if (arguments[1]) {
        result = context.fn(...arguments[1])
      } else {
        result = context.fn()
      }
      delete context.fn

      return result
    }

bind

  Function.prototype.myBind = function(context){
      // 判断是否是一个函数
      if(typeof this !== "function") {
        throw new TypeError("Not a Function")
      }
      // 保存调用bind的函数
      const _this = this 
      // 保存参数
      const args = Array.prototype.slice.call(arguments,1)
      // 返回一个函数
      return function F () {
        // 判断是不是new出来的
        if(this instanceof F) {
          // 如果是new出来的
          // 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,且完成函数柯里化
          return new _this(...args,...arguments)
        }else{
          // 如果不是new出来的改变this指向,且完成函数柯里化
          return _this.apply(context,args.concat(...arguments))
        }
      } 
    }

8.面试官:字面量创建对象和new创建对象有什么区别,new内部都实现了什么,手写一个new
答:

字面量:

字面量创建对象更简单,方便阅读
不需要作用域解析,速度更快
new内部:

创建一个新对象
使新对象的__proto__指向原函数的prototype
改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result
判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result
手写new

  // 手写一个new
    function myNew(fn, ...args) {
      // 创建一个空对象
      let obj = {}
      // 使空对象的隐式原型指向原函数的显式原型
      obj.__proto__ = fn.prototype
      // this指向obj
      let result = fn.apply(obj, args)
      // 返回
      return result instanceof Object ? result : obj
    }

9.面试官:字面量new出来的对象和 Object.create(null)创建出来的对象有什么区别

答:

字面量和new创建出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型,

而 Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性

执行栈和执行上下文

10.面试官:什么是作用域,什么是作用域链

答:

规定变量和函数的可使用范围称作作用域
每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链。

11.面试官:什么是执行栈,什么是执行上下文

答:

执行上下文分为:

全局执行上下文
创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
函数执行上下文
每次函数调用时,都会新创建一个函数执行上下文
执行上下文分为创建阶段和执行阶段
创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
eval执行上下文
执行栈:

首先栈特点:先进后出
当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈。
栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
只有浏览器关闭的时候全局执行上下文才会弹出
闭包
很多人都吃不透js闭包,这里推荐一篇文章:彻底理解js中的闭包

12.面试官:什么是闭包?闭包的作用?闭包的应用

答:

函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用

作用:

保护
避免命名冲突
保存
解决循环绑定引发的索引问题
变量不会销毁
可以使用函数内部的变量,使变量不会被垃圾回收机制回收
应用:

设计模式中的单例模式
for循环中的保留i的操作
防抖和节流
函数柯里化
缺点

会出现内存泄漏的问题
13.原型和原型链
面试官:什么是原型?什么是原型链?如何理解

答:

原型: 原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。

原型链: 多个__proto__组成的集合成为原型链

所有实例的__proto__都指向他们构造函数的prototype
所有的prototype都是对象,自然它的__proto__指向的是Object()的prototype
所有的构造函数的隐式原型指向的都是Function()的显示原型
Object的隐式原型是null

14.继承
面试官:说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点。

答:

原型继承、组合继承、寄生组合继承、ES6的extend

原型继承

  // ----------------------方法一:原型继承
    // 原型继承
    // 把父类的实例作为子类的原型
    // 缺点:子类的实例共享了父类构造函数的引用属性   不能传参

    var person = {
      friends: ["a", "b", "c", "d"]
    }

    var p1 = Object.create(person)

    p1.friends.push("aaa")//缺点:子类的实例共享了父类构造函数的引用属性

    console.log(p1);
    console.log(person);//缺点:子类的实例共享了父类构造函数的引用属性

组合继承

 // ----------------------方法二:组合继承
    // 在子函数中运行父函数,但是要利用call把this改变一下,
    // 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor

    // 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
    // 优点可传参,不共享父类引用属性
    function Father(name) {
      this.name = name
      this.hobby = ["篮球", "足球", "乒乓球"]
    }

    Father.prototype.getName = function () {
      console.log(this.name);
    }

    function Son(name, age) {
      Father.call(this, name)
      this.age = age
    }

    Son.prototype = new Father()
    Son.prototype.constructor = Son


    var s = new Son("ming", 20)

    console.log(s);

寄生组合继承`

   // ----------------------方法三:寄生组合继承
    function Father(name) {
      this.name = name
      this.hobby = ["篮球", "足球", "乒乓球"]
    }

    Father.prototype.getName = function () {
      console.log(this.name);
    }

    function Son(name, age) {
      Father.call(this, name)
      this.age = age
    }

    Son.prototype = Object.create(Father.prototype)
    Son.prototype.constructor = Son

    var s2 = new Son("ming", 18)
    console.log(s2);

extend`

 // ----------------------方法四:ES6的extend(寄生组合继承的语法糖)
    //     子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
    // 必须是 super 。

    class Son3 extends Father { // Son.prototype.__proto__ = Father.prototype
      constructor(y) {
        super(200)  // super(200) => Father.call(this,200)
        this.y = y
      }
    }

15.内存泄露、垃圾回收机制
面试官:什么是内存泄漏

答:

内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏

16.面试官:为什么会导致的内存泄漏

答:

内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃

17.面试官:垃圾回收机制都有哪些策略?

答:

标记清除法
垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的(不可达的)进行删除,进入执行环境的不能进行删除
引用计数法
当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
缺点: 当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代。

18.深拷贝和浅拷贝
手写浅拷贝深拷贝

  // ----------------------------------------------浅拷贝
    // 只是把对象的属性和属性值拷贝到另一个对象中
    var obj1 = {
      a: {
        a1: { a2: 1 },
        a10: { a11: 123, a111: { a1111: 123123 } }
      },
      b: 123,
      c: "123"
    }
    // 方式1
    function shallowClone1(o) {
      let obj = {}

      for (let i in o) {
        obj[i] = o[i]
      }
      return obj
    }

    // 方式2
    var shallowObj2 = { ...obj1 }

    // 方式3
    var shallowObj3 = Object.assign({}, obj1)

    let shallowObj = shallowClone1(obj1);

    shallowObj.a.a1 = 999
    shallowObj.b = true

    console.log(obj1);  //第一层的没有被改变,一层以下就被改变了



    // ----------------------------------------------深拷贝

    // 简易版  
    function deepClone(o) {
      let obj = {}
      for (var i in o) {
        // if(o.hasOwnProperty(i)){
        if (typeof o[i] === "object") {
          obj[i] = deepClone(o[i])
        } else {
          obj[i] = o[i]
        }
        // }
      }
      return obj
    }


    var myObj = {
      a: {
        a1: { a2: 1 },
        a10: { a11: 123, a111: { a1111: 123123 } }
      },
      b: 123,
      c: "123"
    }

    var deepObj1 = deepClone(myObj)
    deepObj1.a.a1 = 999
    deepObj1.b = false
    console.log(myObj);



    // 简易版存在的问题:参数没有做检验,传入的可能是 Array、null、regExp、Date
    function deepClone2(o) {
      if (Object.prototype.toString.call(o) === "[object Object]") {  //检测是否为对象
        let obj = {}
        for (var i in o) {
          if (o.hasOwnProperty(i)) {
            if (typeof o[i] === "object") {
              obj[i] = deepClone(o[i])
            } else {
              obj[i] = o[i]
            }
          }
        }
        return obj
      } else {
        return o
      }
    }

    function isObject(o) {
      return Object.prototype.toString.call(o) === "[object Object]" || Object.prototype.toString.call(o) === "[object Array]"
    }

    // 继续升级,没有考虑到数组,以及ES6中的map、set、weakset、weakmap
    function deepClone3(o) {
      if (isObject(o)) {//检测是否为对象或者数组
        let obj = Array.isArray(o) ? [] : {}
        for (let i in o) {
          if (isObject(o[i])) {
            obj[i] = deepClone(o[i])
          } else {
            obj[i] = o[i]
          }
        }
        return obj
      } else {
        return o
      }
    }


    // 有可能碰到循环引用问题  var a = {}; a.a = a; clone(a);//会造成一个死循环
    // 循环检测
    // 继续升级
    function deepClone4(o, hash = new map()) {
      if (!isObject(o)) return o//检测是否为对象或者数组
      if (hash.has(o)) return hash.get(o)
      let obj = Array.isArray(o) ? [] : {}

      hash.set(o, obj)
      for (let i in o) {
        if (isObject(o[i])) {
          obj[i] = deepClone4(o[i], hash)
        } else {
          obj[i] = o[i]
        }
      }
      return obj
    }

    // 递归易出现爆栈问题
    //  将递归改为循环,就不会出现爆栈问题了
    var a1 = { a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' };
    var b1 = { b: { c: { d: 1 } } }
    function cloneLoop(x) {
      const root = {};
      // 栈 
      const loopList = [  //->[]->[{parent:{a:1,b:2},key:c,data:{ c1: 3, c2: { c21: 4, c22: 5 } }}]
        {
          parent: root,
          key: undefined,
          data: x,
        }
      
      while (loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent; //{} //{a:1,b:2}
        const key = node.key; //undefined //c
        const data = node.data; //{ a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' }  //{ c1: 3, c2: { c21: 4, c22: 5 } }}
        // 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
        let res = parent; //{}->{a:1,b:2,d:'asd'} //{a:1,b:2}->{}
        if (typeof key !== 'undefined') {
          res = parent[key] = {};
        }
        for (let k in data) {
          if (data.hasOwnProperty(k)) {
            if (typeof data[k] === 'object') {
              // 下一次循环 
              loopList.push({
                parent: res,
                key: k,
                data: data[k],
              })
            } else {
              res[k] = data[k];
            }
          }
        }
      }
      return root
    }


    function deepClone5(o) {
      let result = {}
      let loopList = [
        {
          parent: result,
          key: undefined,
          data: o
        }
      ]

      while (loopList.length) {
        let node = loopList.pop()
        let { parent, key, data } = node
        let anoPar = parent
        if (typeof key !== 'undefined') {
          anoPar = parent[key] = {}
        }

        for (let i in data) {
          if (typeof data[i] === 'object') {
            loopList.push({
              parent: anoPar,
              key: i,
              data: data[i]
            })
          } else {
            anoPar[i] = data[i]
          }
        }
      }
      return result
    }


    let cloneA1 = deepClone5(a1)
    cloneA1.c.c2.c22 = 5555555
    console.log(a1);
    console.log(cloneA1);


    // ------------------------------------------JSON.stringify()实现深拷贝

    function cloneJson(o) {
      return JSON.parse(JSON.stringify(o))
    }

    // let obj = { a: { c: 1 }, b: {} };
    // obj.b = obj;
    // console.log(JSON.parse(JSON.stringify(obj))) // 报错 // Converting circular structure to JSON

19.单线程,同步异步
面试官:为什么JS是单线程的?

**答:**因为JS里面有可视的Dom,如果是多线程的话,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的

面试官:如何实现异步编程?

**答:**回调函数

面试官:Generator是怎么样使用的以及各个阶段的变化如何?⭐⭐⭐

答:

首先生成器是一个函数,用来返回迭代器的

调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的

通过调用迭代器的next方法来请求一个一个的值,返回的对象有两个属性,一个是value,也就是值;另一个是done,是个布尔类型,done为true说明生成器函数执行完毕,没有可返回的值了,

done为true后继续调用迭代器的next方法,返回值的value为undefined

状态变化:

每当执行到yield属性的时候,都会返回一个对象
这时候生成器处于一个非阻塞的挂起状态
调用迭代器的next方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行
直到遇到下一次yield依次循环
直到代码没有yield了,就会返回一个结果对象done为true,value为undefined
面试官:说说 Promise 的原理?你是如何理解 Promise 的?⭐⭐⭐⭐⭐

做到会写简易版的promise和all函数就可以
答:

class MyPromise2 {
      constructor(executor) {
        // 规定状态
        this.state = "pending"
        // 保存 `resolve(res)` 的res值
        this.value = undefined
        // 保存 `reject(err)` 的err值
        this.reason = undefined
        // 成功存放的数组
        this.successCB = []
        // 失败存放的数组
        this.failCB = []


        let resolve = (value) => {
          if (this.state === "pending") {
            this.state = "fulfilled"
            this.value = value
            this.successCB.forEach(f => f())
          }
        }
        let reject = (reason) => {
          if (this.state === "pending") {
            this.state = "rejected"
            this.value = value
            this.failCB.forEach(f => f())
          }
        }

        try {
          // 执行
          executor(resolve, reject)
        } catch (error) {
          // 若出错,直接调用reject
          reject(error)
        }
      }
      then(onFulfilled, onRejected) {
        if (this.state === "fulfilled") {
          onFulfilled(this.value)
        }
        if (this.state === "rejected") {
          onRejected(this.value)
        }
        if (this.state === "pending") {
          this.successCB.push(() => { onFulfilled(this.value) })
          this.failCB.push(() => { onRejected(this.reason) })
        }
      }
    }


    Promise.all = function (promises) {
      let list = []
      let count = 0
      function handle(i, data) {
        list[i] = data
        count++
        if (count == promises.length) {
          resolve(list)
        }
      }
      return Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
          promises[i].then(res => {
            handle(i, res)
          }, err => reject(err))
        }
      })
    }

async function async1() {
   console.log('async1 start')
   await async2()
   console.log('async1 end')
  }
  async function async2() {
   console.log('async2')
  }
  async1()
  console.log('script start')

//执行到await时,如果返回的不是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部继续执行
//执行到await时,如果返回的是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码
//所以结果为
//async1 start
//async2
//script start
//async1 end

20.面试官:宏任务和微任务都有哪些

答:

宏任务:script、setTimeOut、setInterval、setImmediate
微任务:promise.then,process.nextTick、Object.observe、MutationObserver
注意:Promise是同步任务
面试官:宏任务和微任务都是怎样执行的

答:

执行宏任务script,
进入script后,所有的同步任务主线程执行
所有宏任务放入宏任务执行队列
所有微任务放入微任务执行队列
先清空微任务队列,
再取一个宏任务,执行,再清空微任务队列
依次循环
21.面试官:变量和函数怎么进行提升的?优先级是怎么样的?
答:

对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值
开辟堆空间
存储内容
将地址赋给变量
对变量进行提升,只声明,不赋值,值为undefined

22.面试官:var let const 有什么区别

答:

var
var声明的变量可进行变量提升,let和const不会
var可以重复声明
var在非函数作用域中定义是挂在到window上的
let
let声明的变量只在局部起作用
let防止变量污染
不可在声明
const
具有let的所有特征
不可被改变
如果使用const声明的是对象的话,是可以修改对象里面的值的

23.面试官:箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗

箭头函数是普通函数的简写,但是它不具备很多普通函数的特性
第一点,this指向问题,箭头函数的this指向它定义时所在的对象,而不是调用时所在的对象
不会进行函数提升
没有arguments对象,不能使用arguments,如果要获取参数的话可以使用rest运算符
没有yield属性,不能作为生成器Generator使用
不能new
没有自己的this,不能调用call和apply
没有prototype,new关键字内部需要把新对象的_proto_指向函数的prototype
面试官:说说你对代理的理解

代理有几种定义方式
字面量定义,对象里面的 get和set
类定义, class 中的get和set
Proxy对象,里面传两个对象,第一个对象是目标对象target,第二个对象是专门放get和set的handler对象。Proxy和上面两个的区别在于Proxy专门对对象的属性进行get和set
代理的实际应用有
Vue的双向绑定 vue2用的是Object.defineProperty,vue3用的是proxy
校验值
计算属性值(get的时候加以修饰)

24.面试官:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

为什么要使用模块化
防止命名冲突
更好的分离,按需加载
更好的复用性
更高的维护性
面试官:exports和module.exports有什么区别?

导出方式不一样
exports.xxx=‘xxx’
module.export = {}
exports是module.exports的引用,两个指向的是用一个地址,而require能看到的只有module.exports

25.面试官:JS模块包装格式有哪些?

commonjs

同步运行,不适合前端
AMD

异步运行
异步模块定义,主要采用异步的方式加载模块,模块的加载不影响后面代码的执行。所有依赖这个模块的语句都写在一个回调函数中,模块加载完毕,再执行回调函数
CMD

异步运行
seajs 规范

26.面试官:ES6和commonjs的区别

commonjs模块输出的是值的拷贝,而ES6输出的值是值的引用
commonjs是在运行时加载,是一个对象,ES6是在编译时加载,是一个代码块
commonjs的this指向当前模块,ES6的this指向undefined

27.JSONP

JSONP通过同源策略涉及不到的"漏洞",也就是像img中的src,link标签的href,script的src都没有被同源策略限制到

JSONP只能get请求

源码:

 function addScriptTag(src) {
      var script = document.createElement("script")
      script.setAttribute('type','text/javascript')
      script.src = src
      document.appendChild(script)
    }
    //回调函数
    function endFn(res) {
      console.log(res.message);
    }

    // 前后端商量好,后端如果传数据的话,返回`endFn({message:'hello'})`

28.document.domain

只能跨一级域名相同的域(www.qq.om和www.id.qq.com , 二者都有qq.com)

使用方法

表示输入, <表示输出 ,以下是在www.id.qq.com网站下执行的操作

> var w = window.open("https://www.qq.com")
< undefined
> w.document
✖ VM3061:1 Uncaught DOMException: Blocked a frame with origin "https://id.qq.com" from accessing a cross-origin frame.
    at <anonymous>:1:3
> document.domain
< "id.qq.com"
> document.domain = 'qq.com'
< "qq.com"
> w.document
< #document

28.location.hash+iframe

因为hash传值只能单向传输,所有可以通过一个中间网页,a若想与b进行通信,可以通过一个与a同源的c作为中间网页,a传给b,b传给c,c再传回a

具体做法:在a中放一个回调函数,方便c回调。放一个iframe标签,随后传值

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

在b中监听哈希值改变,一旦改变,把a要接收的值传给c

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

在c中监听哈希值改变,一旦改变,调用a中的回调函数

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

29.window.name+iframe

利Access用window.name不会改变(而且很大)来获取数据,
a要获取b的数据,b中把数据转为json格式放到window.name中
postMessage

a窗口向b窗口发送数据,先把data转为json格式,在发送。提前设置好messge监听
b窗口进行message监听,监听到了以同样的方式返回数据,
a窗口监听到message,在进行一系列操作
CORS

通过自定义请求头来让服务器和浏览器进行沟通
有简单请求和非简单请求
满足以下条件,就是简单请求
请求方法是HEAD、POST、GET
请求头只有Accept、AcceptLanguage、ContentType、ContentLanguage、Last-Event-Id
简单请求,浏览器自动添加一个Origin字段
同时后端需要设置的请求头
Access-Control-Allow-Origin --必须
Access-Control-Expose-Headers
XMLHttpRequest只能拿到六个字段,要想拿到其他的需要在这里指定
Access-Control-Allow-Credentials --是否可传cookie
要是想传cookie,前端需要设置xhr.withCredentials = true,后端设置Access-Control-Allow-Credentials
非简单请求,浏览器判断是否为简单请求,如果是非简单请求,则 浏览器先发送一个header头为option的请求进行预检
预检请求格式(请求行 的请求方法为OPTIONS(专门用来询问的))
Origin
Access-Control-Request-Method
Access-Control-Request-Header
浏览器检查了Origin、Access-Control-Allow-Method和Access-Control-Request-Header之后确认允许就可以做出回应了
通过预检后,浏览器接下来的每次请求就类似于简单请求了

30.nginx代理跨域

nginx模拟一个虚拟服务器,因为服务器与服务器之间是不存在跨域的,
发送数据时 ,客户端->nginx->服务端
返回数据时,服务端->nginx->客户端

31.面试官:localStorage、SessionStorage、cookie、session 之间有什么区别

localStorage
生命周期:关闭浏览器后数据依然保留,除非手动清除,否则一直在
作用域:相同浏览器的不同标签在同源情况下可以共享localStorage
sessionStorage
生命周期:关闭浏览器或者标签后即失效
作用域:只在当前标签可用,当前标签的iframe中且同源可以共享
cookie
是保存在客户端的,一般由后端设置值,可以设置过期时间
储存大小只有4K
一般用来保存用户的信息的
在http下cookie是明文传输的,较不安全
cookie属性有
http-only:不能被客户端更改访问,防止XSS攻击(保证cookie安全性的操作)
Secure:只允许在https下传输
Max-age: cookie生成后失效的秒数
expire: cookie的最长有效时间,若不设置则cookie生命期与会话期相同
session
session是保存在服务端的
session的运行依赖sessionId,而sessionId又保存在cookie中,所以如果禁用的cookie,session也是不能用的,不过硬要用也可以,可以把sessionId保存在URL中
session一般用来跟踪用户的状态
session 的安全性更高,保存在服务端,不过一般为使服务端性能更加,会考虑部分信息保存在cookie中

32.怎么使用cookie保存用户信息

document.cookie(“名字 = 数据;expire=时间”)

33.怎么删除cookie

目前没有提供删除的操作,但是可以把它的Max-age设置为0,也就是立马失效,也就是删除了

34.面试官:Get和Post的区别

冪等/不冪等(可缓存/不可缓存)
get请求是冪等的,所以get请求的数据是可以缓存的
而post请求是不冪等的,查询查询对数据是有副作用的,是不可缓存的
传参
get传参,参数是在url中的
准确的说get传参也可以放到body中,只不过不推荐使用
post传参,参数是在请求体中
准确的说post传参也可以放到url中,只不过不推荐使用
安全性
get较不安全
post较为安全
准确的说两者都不安全,都是明文传输的,在路过公网的时候都会被访问到,不管是url还是header还是body,都会被访问到,要想做到安全,就需要使用https
参数长度
get参数长度有限,是较小的
准确来说,get在url传参的时候是很小的
post传参长度不受限制
发送数据
post传参发送两个请求包,一个是请求头,一个是请求体,请求头发送后服务器进行验证,要是验证通过的话就会给客户端发送一个100-continue的状态码,然后就会发送请求体
字符编码
get在url上传输的时候只允许ASCII编码

35面试官:讲讲http缓存

https://www.jianshu.com/p/9c95db596df5

缓存分为强缓存和协商缓存

强缓存

在浏览器加载资源时,先看看cache-control里的max-age,判断数据有没有过期,如果没有直接使用该缓存 ,有些用户可能会在没有过期的时候就点了刷新按钮,这个时候浏览器就回去请求服务端,要想避免这样做,可以在cache-control里面加一个immutable.
public
允许客户端和虚拟服务器缓存该资源,cache-control中的一个属性
private
只允许客户端缓存该资源
no-cache
不允许强缓存,可以协商缓存
no-store
不允许缓存

协商缓存

浏览器加载资源时,没有命中强缓存,这时候就去请求服务器,去请求服务器的时候,会带着两个参数,一个是If-None-Match,也就是响应头中的etag属性,每个文件对应一个etag;另一个参数是If-Modified-Since,也就是响应头中的Last-Modified属性,带着这两个参数去检验缓存是否真的过期,如果没有过期,则服务器会给浏览器返回一个304状态码,表示缓存没有过期,可以使用旧缓存。
etag的作用
有时候编辑了文件,但是没有修改,但是last-modified属性的时间就会改变,导致服务器会重新发送资源,但是etag的出现就完美的避免了这个问题,他是文件的唯一标识

缓存位置:

内存缓存Memory-Cache
离线缓存Service-Worker
磁盘缓存Disk-Cache
推送缓存Push-Cache

36.什么是CDN?

关于 cdn、回源等问题一网打尽

1.首先访问本地的 DNS ,如果没有命中,继续递归或者迭代查找,直到命中拿到对应的 IP 地址。

2.拿到对应的 IP 地址之后服务器端发送请求到目的地址。注意这里返回的不直接是 cdn 服务器的 IP 地址,而是全局负载均衡系统的 IP 地址

4.全局负载均衡系统会根据客户端的 IP地址和请求的 url 和相应的区域负载均衡系统通信

5.区域负载均衡系统拿着这两个东西获取距离客户端最近且有相应资源的cdn 缓存服务器的地址,返回给全局负载均衡系统

6.全局负载均衡系统返回确定的 cdn 缓存服务器的地址给客户端。

7.客户端请求缓存服务器上的文件

37.什么是xss?什么是csrf?

xss脚本注入
不需要你做任何的登录认证,它会通过合法的操作(比如在url中输入、在评论框中输入),向你的页面注入脚本(可能是js、hmtl代码块等)。
防御
编码:对用户输入的数据进行HTML Entity 编码。把字符转换成 转义字符。Encode的作用是将$var等一些字符进行转化,使得浏览器在最终输出结果上是一样的。
过滤:移除用户输入的和事件相关的属性。
csrf跨域请求伪造
在未退出A网站的前提下访问B,B使用A的cookie去访问服务器
防御:token,每次用户提交表单时需要带上token(伪造者访问不到),如果token不合法,则服务器拒绝请求
OWASP top10 (10项最严重的Web应用程序安全风险列表)都有哪些?⭐⭐⭐

SQL注入

在输入框里输入sql命令
失效的身份验证

拿到别人的cookie来向服务端发起请求,就可以做到登陆的目的
敏感数据泄露

明文传输状态下可能被抓包拦截,这时候就造成数据泄露
想做到抓包,比如在网吧,共享一个猫上网,这时候抓包就可行,方法网上一搜一大把
不过此风险大部分网站都能得到很好的解决,https或者md5加密都可以
XML 外部实体

失效的访问控制

安全配置错误

XSS

不安全的反序列化

使用含有已知漏洞的组件

不足的日志记录和监控

38.面试官:什么是回流 什么是重绘?

回流
render树中一部分或全部元素需要改变尺寸、布局、或着需要隐藏而需要重新构建,这个过程叫做回流
回流必将引起重绘
重绘
render树中一部分元素改变,而不影响布局的,只影响外观的,比如颜色。该过程叫做重绘
页面至少经历一次回流和重绘(第一次加载的时候)
杂项

39.事件冒泡和事件捕捉有什么区别

事件冒泡
在addEventListener中的第三属性设置为false(默认)
从下至上(儿子至祖宗)执行
事件捕捉
在addEventListener中的第三属性设置为true
从上至下(祖宗到儿子)执行
什么是防抖?什么是节流?手写一个

40.防抖
n秒后在执行该事件,若在n秒内被重复触发,则重新计时
节流
n秒内只运行一次,若在n秒内重复触发,只有一次生效

// ---------------------------------------------------------防抖函数
function debounce(func, delay) {
  let timeout
  return function () {
    let arg = arguments
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(() => {
      func(arg)
    }, delay);
  }
}

// ---------------------------------------------------------立即执行防抖函数
function debounce2(fn, delay) {
  let timer

  return function () {
    let args = arguments
    if (timer) clearTimeout(timer)


    let callNow = !timer
    timer = setTimeout(() => {
      timer = null
    }, delay);
    if (callNow) { fn(args) }
  }
}
// ---------------------------------------------------------立即执行防抖函数+普通防抖
function debounce3(fn, delay, immediate) {
  let timer

  return function () {
    let args = arguments
    let _this = this
    if (timer) clearTimeout(timer)

    if (immediate) {
      let callNow = !timer
      timer = setTimeout(() => {
        timer = null
      }, delay);

      if (callNow) { fn.apply(_this, args) }
    } else {
      timeout = setTimeout(() => {
        func.apply(_this, arguments)
      }, delay);
    }
  }
}

// ---------------------------------------------------------节流 ,时间戳版

function throttle(fn, wait) {

  let previous = 0
  return function () {
    let now = Date.now()
    let _this = this
    let args = arguments
    if (now - previous > wait) {
      fn.apply(_this, arguments)
      previous = now
    }
  }
}

// ---------------------------------------------------------节流 ,定时器版
function throttle2(fn, wait) {
  let timer
  return function () {
    let _this = this
    let args = arguments
    if (!timer) {
      timer = setTimeout(() => {
        timer = null
        fn.apply(_this, arguments)
      }, wait);
    }
  }
}

函数柯里化原理

**function add() {
  var args = Array.prototype.slice.call(arguments)
  var adder = function () {
    args.push(...arguments)
    return adder
  }
  adder.toString = function () {
    return args.reduce((prev, curr) => {
      return prev + curr
    }, 0)
  }
  return adder
}
let a = add(1, 2, 3)
let b = add(1)(2)(3)
console.log(a)
console.log(b)
console.log(add(1, 2)(3));
console.log(Function.toString)**

41.jS性能优化的方式

垃圾回收
闭包中的对象清楚
防抖节流
分批加载(setInterval,加载10000个节点)
事件委托
少用with
requestAnimationFrame的使用
script标签中的defer和async
CDN
Vue
Vue双向绑定
数据劫持: vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调

42.阐述一下你所理解的MVVM响应式原理

vue是采用数据劫持配合发布者-订阅者的模式的方式,通过Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时,发布消息给依赖收集器(dep中的subs),去通知(notify)观察者,做出对应的回调函数,去更新视图

MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通信桥路,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果。

杂乱笔记

data中每一个数据都绑定一个Dep,这个Dep中都存有所有用到该数据的观察者

当数据改变时,发布消息给dep(依赖收集器),去通知每一个观察者。做出对应的回调函数

59图片懒加载
路由懒加载

为减少重新渲染和创建dom节点的时间,采用虚拟dom

请说明px,em,rem,vw,vh,rpx等单位的特性

px
像素
em
当前元素的字体大小
rem
根元素字体大小
vw
100vw是总宽度
vh
100vh是总高度
rpx
750rpx是总宽度

什么是BFC?

BFC是一个独立渲染区域,它丝毫不会影响到外部元素
BFC特性
同一个BFC下margin会重叠
计算BFC高度时会算上浮动元素
BFC不会影响到外部元素
BFC内部元素是垂直排列的
BFC区域不会与float元素重叠
如何创建BFC
position设为absolute或者fixed
float不为none
overflow设置为hidden
display设置为inline-block或者inline-table或flex

什么是DOM事件流?什么是事件委托

DOM事件流
分为三个阶段
捕获阶段
目标阶段
冒泡阶段
在addeventListener()的第三个参数(useCapture)设为true,就会在捕获阶段运行,默认是false冒泡
事件委托
利用冒泡原理(子向父一层层穿透),把事件绑定到父元素中,以实现事件委托

link标签和import标签的区别

link属于html,而@import属于css
页面被加载时,link会同时被加载,而@import引用的css会等到页面加载结束后加载。
link是html标签,因此没有兼容性,而@import只有IE5以上才能识别。
link方式样式的权重高于@import的。

算法
这里推荐一个排序算法的动画网站,应该是一个国外团队做的,Sorting Algorithms

冒泡算法排序

  // 冒泡排序
    /* 1.比较相邻的两个元素,如果前一个比后一个大,则交换位置。

   2.第一轮的时候最后一个元素应该是最大的一个。

   3.按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。 */
    function bubbleSort(arr) {
      for (var i = 0; i < arr.length; i++) {
        for (var j = 0; j < arr.length; j++) {
          if (arr[j] > arr[j + 1]) {
            var temp = arr[j]
            arr[j] = arr[j + 1]
            arr[j + 1] = temp
          }
        }
      }
    }

    var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
    console.log(Arr, "before");
    bubbleSort(Arr)
    console.log(Arr, "after");

快速排序

 /*
    快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。
    然后递归调用,在两边都实行快速排序。  
    */
    
    function quickSort(arr) {
      if (arr.length <= 1) {
        return arr
      }
      var middle = Math.floor(arr.length / 2)
      var middleData = arr.splice(middle, 1)[0]

      var left = []
      var right = []
      
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] < middleData) {
          left.push(arr[i])
        } else {
          right.push(arr[i])
        }
      }

      return quickSort(left).concat([middleData], quickSort(right))
    }

    var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
    console.log(Arr, "before");
    var newArr = quickSort(Arr)
    console.log(newArr, "after");

*插入排序

function insertSort(arr) {
  // 默认第一个排好序了
  for (var i = 1; i < arr.length; i++) {
    // 如果后面的小于前面的直接把后面的插到前边正确的位置
    if (arr[i] < arr[i - 1]) {
      var el = arr[i]
      arr[i] = arr[i - 1]
      var j = i - 1
      while (j >= 0 && arr[j] > el) {
        arr[j+1] = arr[j]
        j--
      }
      arr[j+1] = el
    }
  }
}

var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
console.log(Arr, "before");
insertSort(Arr)
console.log(Arr, "after");

是否回文

function isHuiWen(str) {
return str == str.split("").reverse().join("")
}

console.log(isHuiWen(“mnm”));

正则表达式,千分位分隔符

**function thousand(num) {
  return (num+"").replace(/\d(?=(\d{3})+$)/g, "$&,")
}
console.log(thousand(123456789));**

斐波那契数列

// num1前一项
// num2当前项
function fb(n, num1 = 1, num2 = 1) {
  if(n == 0) return 0
  if (n <= 2) {
    return num2
  } else {
    return fb(n - 1, num2, num1 + num2)
  }
}

数组去重的方式

var arr = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]

// 最low1
let newArr2 = []
for (let i = 0; i < arr.length; i++) {
  if (!newArr2.includes(arr[i])) {
    newArr2.push(arr[i])
  }
}
console.log(newArr2);
// 最low2
let arr2 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
for (let i = 0; i < arr2.length; i++) {
  var item = arr2[i]
  for (let j = i + 1; j < arr2.length; j++) {
    var compare = arr2[j];
    if (compare === item) {
      arr2.splice(j, 1)
      j--
    }
  }
}
console.log(arr2);

```javascript
 // 基于对象去重
    let arr3 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
    let obj = {}
    for (let i = 0; i < arr3.length; i++) {

      let item = arr3[i]
      if (obj[item]) {
        arr3[i] = arr3[arr3.length - 1]
        arr3.length--
        i--
        continue;
      }
      obj[item] = item

    }
    console.log(arr3);
    console.log(obj);

    // 利用Set
    let newArr1 = new Set(arr)
    console.log([...newArr1]);
    let arr4 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]

    //利用reduce
    newArr4 = arr4.reduce((prev, curr) => prev.includes(curr)? prev : [...prev,curr],[])
    console.log(newArr4);
    console.log(document);

ES6

1、使用箭头函数应注意什么?

1)用了箭头函数,this就不是指向window,而是父级(指向是可变的)
(2)不能够使用arguments对象
(3)不能用作构造函数,这就是说不能够使用new命令,否则会抛出一个错误
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数

2、ES6的模板字符串有哪些新特性?并实现一个类模板字符串的功能

基本的字符串格式化。
将表达式嵌入字符串中进行拼接。
用${}来界定在ES5时我们通过反斜杠()来做多行字符串或者字符串一行行拼接。
ES6反引号(``)就能解决类模板字符串的功能
let name = 'web';
let age = 10;
let str = '你好,${name} 已经 ${age}岁了'
str = str.replace(/\$\{([^}]*)\}/g,function(){
     return eval(arguments[1]);
   })
console.log(str);//你好,web 已经 10岁了

3、介绍下 Set、Map的区别?

应用场景Set用于数据重组,Map用于数据储存Set: 
(1)成员不能重复
(2)只有键值没有键名,类似数组
(3)可以遍历,方法有add, delete,has
Map:
(1)本质上是健值对的集合,类似集合
(2)可以遍历,可以跟各种数据格式转换

4、ECMAScript 6 怎么写 class ,为何会出现 class?
ES6的class可以看作是一个语法糖,它的绝大部分功能ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法

//定义类
class Point { 
  constructor(x,y) { 
      //构造方法
       this.x = x; //this关键字代表实例对象
       this.y = y; 
  } toString() {
       return '(' + this.x + ',' + this.y + ')'; 
  }
}

5、Promise构造函数是同步执行还是异步执行,那么 then 方法呢?

promise构造函数是同步执行的,then方法是异步执行的

6、setTimeout、Promise、Async/Await 的区别
事件循环中分为宏任务队列和微任务队列
其中setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行async函数表示函数里面可能会有异步方法,await后面跟一个表达式
async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行

7、promise有几种状态,什么时候会进入catch?

三个状态:
pending、fulfilled、reject
两个过程:
padding -> fulfilled、padding -> rejected当pending为rejectd时,会进入catch

8.Promise 中reject 和 catch 处理上有什么区别

reject 是用来抛出异常,catch 是用来处理异常
reject 是 Promise 的方法,而 catch 是 Promise 实例的方法
reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch
网络异常(比如断网),会直接进入catch而不会进入then的第二个回调

9 es6新增字符串方法

  • includes()还回布尔值,表示是否找到了参数字符串
  • startsWith()还回布尔值,表示参数字符串是否在源字符串的头部
  • endsWith()还回布尔值,表示参数字符串是否在源字符串的尾部
  • repeat

10 es6新增数组方法

  • map()表示循环生成一个新的数组
  • filter() 条件过滤筛选出符合条件的数据生成新的数组
  • every()判断数组中的每一项是否符合条件,有一项不符合就是false
  • some()判断数组中的每一项是否符合条件

11 对象方法

  • Object.is()用于判断
  • Object.assign()方法是复制而不是深复制
  • Object.values,Object.key,Object.entirs 对象变数组
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值