2020前端复习点

352 阅读12分钟

整理了一些复习知识点,有很多是借鉴别人的文章

JavaScript

1. 对象的声明方式?

  • 字面量的方式声明对象;
var obj = {
    属性名称:属性值,
    方法名称:function (){
        //函数执行体
    }
}
  • new 操作符 + Object声明对象;
var obj = new Object();
obj.属性名称 = 属性值;
obj.方法名称 = function (){
            //函数执行体
}
  • 构造函数声明对象;
function text([参数列表]){
    this.属性名称 = 属性值;
    this.方法名称 = function (){
    //函数执行体
    }
}
var obj = new text(参数);
  • 工厂方式声明对象;
function text(name, age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.run = function (){
        return this.name + this.age;  //要return
    }
}
var obj1 = text('zhaoweinian', 21);
var obj2 = text('lisi', 30);
  • 原型模式声明对象;
function text(name, age){
    text.prototype.属性名称 = 属性值;
    text.prototype.方法名称 = function (){
        //函数执行体
    }
}
var obj = new text('lisi', 20);
  • 混合模式声明对象;
function text(name, age){
  this.name = name;
  this.age = age;
}
text.prototype.属性名称 = 属性值;
text.prototype.方法名称 = function (){
    //函数执行体
}
var obj = new text('zhangsan', 30);

2.继承的方法?

  • 1、原型链继承
// 缺点:所有属性被共享,而且不能传递参 
function Person(name,age){
  this.name = name
  this.age = age
}
Person.prototype.sayName = () =>{
  console.log(this.name)
}
function Man(name){

}
Man.prototype = new Person()
Man.prototype.name = 'zhangsan'
var zhangsan = new Man('zhangsan')
console.log(zhangsan.name) //zhangsan

  • 2、构造函数的继承(经典继承)
/* 
  优点:可以传递参数
  缺点:所有方法都在构造函数内,每次创建对象都会创建对应的方法,大大浪费内存
*/
function Perent(name,age,sex){
  this.name = name
  this.age = age
  this.sex = sex
  this.sayName = function(){
    console.log(this.name)
  }
}
function Child(name,age,sex){
  Perent.call(this,name,age,sex)
}
let child = new Child('lisi' , 18, '男')
console.log(child)   //Child { name: 'lisi', age: 18, sex: '男', sayName: [Function] }
  • 3、组合方式继承(构造函数+原型链)
/* 
  这种方式充分利用了原型链与构造函数各自的优点,是JS中最常用的继承方法
*/
function Animal(name,age){
  this.name = name
  this.age = age
}
Animal.prototype.sayName = function () {
  console.log(this.name)
}
function Cat(name,age,color){
  Animal.call(this,name,age)
  this.color = color
}
Cat.prototype = Animal.prototype  //将Cat的原型指向Animal的原型
Cat.prototype.constructor = Cat   //将Cat的构造函数指向Cat
let cat = new Cat('xiaobai',3,'white')
console.log(cat) //Cat { name: 'xiaobai', age: 3, color: 'white' }
cat.sayName()   //xiaobai
  • 4、es6方法继承
class Per {
  constructor(name){
    this.name = name
  }
  sayName(){
    console.log(this.name)
  }
}

class Son extends Per{
  constructor(name,age){
    super(name)
    this.age = age
  }
}
let son = new Son('zhangsan',18)
console.log(son) //Son { name: 'zhangsan', age: 18 }
son.sayName() //zhangsan

3. 什么是闭包?

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

闭包有两个常用的用途。

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

function a(){
    var n = 0;
    function add(){
       n++;
       console.log(n);
    }
    return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1();    //1
a1();    //2  第二次调用n变量还在内存中

其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。

优缺点:
    优点:1:变量长期驻扎在内存中;2:避免全局变量的污染;3:私有成员的存在 ;
    缺点:1.导致内存泄漏;2.会改变父函数内部变量的值
产生内存泄漏的原因: 如果一个对象不再被引用,那么这个对象就会被 GC回收,否则这个对象一直会保存在内存中

4. 什么是构造函数?

constructor返回创建实例对象时构造函数的引用。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。

function Parent(age) {
    this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false

5. 什么是原型?

  • 每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。

6. 什么是原型链?

  • 每个对象拥有一个原型对象,通过 proto 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。

7. 浅拷贝

1、什么是浅拷贝?

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

上图中,SourceObject 是原对象,其中包含基本类型属性 field1 和引用类型属性 refObj。浅拷贝之后基本类型数据 field2 和 filed1 是不同属性,互不影响。但引用类型 refObj 仍然是同一个,改变之后会对另一个对象产生影响。

简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

2、浅拷贝应用场景

  • Object.assign() Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = Object.assign({}, a);
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 book 时,对象 b 相应的位置也发生了变化。

  • Array.prototype.slice()
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [4, 3]]

可以看出,改变 a[1] 之后 b[0] 的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝,相应的还有concat等,在工作中面对复杂数组结构要额外注意。

8. 深拷贝

1、什么是深拷贝?

  • 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

2、深拷贝使用场景

  • JSON.parse(JSON.stringify(object))
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

完全改变变量 a 之后对 b 没有任何影响,这就是深拷贝的魔力。

9. JavaScript的数据类型?

1、基本类型

  • null
  • undefined
  • boolean
  • number
  • string
  • symbol

2、引用类型

  • Object

10. 谈谈你对this、call、apply和bind的理解

  1. 在浏览器里,在全局范围内this 指向window对象;
  2. 在函数中,this永远指向最后调用他的那个对象;
  3. 构造函数中,this指向new出来的那个新的对象;
  4. call、apply、bind中的this被强绑定在指定的那个对象上;
  5. 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
  6. apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。

11. typeof和instanceof的区别?

  • typeof表示是对某个变量类型的检测,基本数据类型除了null都能正常的显示为对应的类型,引用类型除了函数会显示为'function',其它都显示为object
  • instanceof它主要是用于检测某个构造函数的原型对象在不在某个对象的原型链上,返回值为true或false。

12. 数组去重并重新排序

  • Array.from(new Set(arr)) 或 [...new Set(arr)]

let arr = [1,1,2,2,5,4,5,11,6,3,15,5,5,6,8,9,8];
let newArr = Array.from(new Set(arr)).sort((a, b) => a - b)
console.log(newArr) //[1, 2, 3, 4, 5, 6, 8, 9, 11, 15]
// console.log([...new Set(arr)].sort((a, b) => a - b))

13. (ES6)有哪些新特性?

  • 块作用域
  • 箭头函数
  • 模板刻字符
  • 加强的对象字面
  • 对象解构
  • Promise
  • Symbol
  • 函数默认参数
  • rest和展开

14. var,let和const的区别是什么?

  • var声明的变量会挂载在window上,而let和const声明的变量不会:

var a = 100;
console.log(a,window.a);    // 100 100

let b = 10;
console.log(b,window.b);    // 10 undefined

const c = 1;
console.log(c,window.c);    // 1 undefined

  • var声明变量存在变量提升,let和const不存在变量提升:

console.log(a); // undefined  ===>  a已声明还没赋值,默认得到undefined值
var a = 100;

console.log(b); // 报错:b is not defined  ===> 找不到b这个变量
let b = 10;

console.log(c); // 报错:c is not defined  ===> 找不到c这个变量
const c = 10;

  • let和const声明形成块作用域

if(1){
  var a = 100;
  let b = 10;
}

console.log(a); // 100
console.log(b)  // 报错:b is not defined  ===> 找不到b这个变量

-------------------------------------------------------------

if(1){
  var a = 100;
  const c = 1;
}
console.log(a); // 100
console.log(c)  // 报错:c is not defined  ===> 找不到c这个变量

  • 同一作用域下let和const不能声明同名变量,而var可以

var a = 100;
console.log(a); // 100

var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;

//  控制台报错:Identifier 'a' has already been declared  ===> 标识符a已经被声明了。

  • 暂存死区

var a = 100;
if(1){
    a = 10;
    //在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
    // 而这时,还未到声明时候,所以控制台Error:a is not defined
    let a = 1;
}

  • const一旦声明必须赋值,不能使用null占位,声明后不能再修改。

15. 什么是箭头函数?

  • 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
  • 箭头函数没有自己的this值,它捕获词法作用域函数的this值。如果我们在全局作用域声明箭头函数,则this值为 window 对象。

//ES5 
function greet(name) {
  return 'Hello ' + name + '!';
}

//ES6 
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;

16. 什么是类?

  • 类(class)是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。

17. 什么是Promise?

  • Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。由于它的then方法和catch、finally方法会返回一个新的Promise所以可以允许我们链式调用,解决了传统的回调地狱问题。
  • Promise有三种状态:pending(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再变。创造Promise实例后,它会立即执行。

18. 什么是 async/await 及其如何工作,有什么优缺点?

  • async/await是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise和回调,它的可读性和简洁度都更高。
  • async 是异步的意思,而 await 是 async wait的简写,即异步等待。 所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。 一个函数如果加上 async ,那么该函数就会返回一个 Promise

async function test() {
  return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}

可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve()封装成Promise对象返回。

相比于 Promise,async/await能更好地处理 then 链


function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在分别用 Promise 和async/await来实现这三个步骤的处理。

使用Promise


function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
        });
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900

使用async/await


async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
}
doIt();

await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。 await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。

优缺点:

  • async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。
  • await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

19. JavaScript的防抖和节流

1、什么是防抖?

  • 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

适用场景

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

手写简化版:

// 防抖函数
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

复杂版:


// fn 是需要防抖处理的函数
// wait 是时间间隔
function debounce(fn, wait = 50) {
    // 通过闭包缓存一个定时器 id
    let timer = null
    // 将 debounce 处理结果当作函数返回
    // 触发事件回调时执行这个返回函数
    return function(...args) {
      	// 如果已经设定过定时器就清空上一次的定时器
        if (timer) clearTimeout(timer)
      
      	// 开始设定一个新的定时器,定时器结束后执行传入的函数 fn
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, wait)
    }
}

// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

2、什么是节流?

  • 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

简化版


// 节流函数
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

复杂版


// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
  // 上一次执行 fn 的时间
  let previous = 0
  // 将 throttle 处理结果当作函数返回
  return function(...args) {
    // 获取当前时间,转换成时间戳,单位毫秒
    let now = +new Date()
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

20.描述一下EventLoop的执行过程

  • 一开始整个脚本作为一个宏任务执行

  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列

  • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完

  • 执行浏览器UI线程的渲染工作

  • 检查是否有Web Worker任务,有则执行

Vue

1.MVVM是什么?

  • MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

2.生命周期是什么

  • Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期。

3.异步请求适合在哪个生命周期调用?

官方实例的异步请求是在mounted生命周期中调用的,而实际上也可以在created生命周期中调用。

4.Vue是如何实现双向绑定的?

  • 利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,在vue3.0中通过Proxy代理对象进行类似的操作。

// 这是将要被劫持的对象
const data = {
  name: '',
};

function say(name) {
  if (name === '古天乐') {
    console.log('给大家推荐一款超好玩的游戏');
  } else if (name === '渣渣辉') {
    console.log('戏我演过很多,可游戏我只玩贪玩懒月');
  } else {
    console.log('来做我的兄弟');
  }
}

// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function(key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('get');
    },
    set: function(newVal) {
      // 当属性值发生变化时我们可以进行额外操作
      console.log(`大家好,我系${newVal}`);
      say(newVal);
    },
  });
});

data.name = '渣渣辉';
//大家好,我系渣渣辉
//戏我演过很多,可游戏我只玩贪玩懒月

5.computed和watch有什么区别?

computed:

  1. computed是计算属性,也就是计算值,它更多用于计算值的场景
  2. computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算
  3. computed适用于计算比较消耗性能的计算场景

watch:

  1. 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作
  2. 无缓存性,页面重新渲染时值不变化也会执行

小结:

  1. 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
  2. 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化

6. v-if、v-show、v-html 的原理是什么,它是如何封装的?

  • v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
  • v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;
  • v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。

v-if和v-show的区别

当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。

7. Vue中的key到底有什么用?

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速: 利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

8. nextTick 的实现原理?

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。nextTick主要使用了宏任务和微任务。

根据执行环境分别尝试采用

  • Promise
  • MutationObserver
  • setImmediate
  • 如果以上都不行则采用setTimeout

定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

9. Vue组件如何通信?

  • 父子组件通信: props/$emit + $on: 通过props将数据自上而下传递,而通过$emit$on来向上传递信息。
  • 兄弟组件通信:EventBus: 通过EventBus进行信息的发布与订阅。
  • 跨级组件通信:
    1. vuex: 是全局数据管理库,可以通过vuex管理全局的数据流
    2. $attrs/$listeners: Vue2.4中加入的$attrs/$listeners可以进行跨级的组件通信
    3. provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础

10.Vue事件绑定原理?

  • 原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。

11. hash路由和history路由实现原理?

  • keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
  • 常用的两个属性include/exclude,允许组件有条件的进行缓存。
  • 两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
  • keep-alive的中还运用了LRU(Least Recently Used)算法。

12.hash路由和history路由实现原理?

  • location.hash的值实际就是URL中#后面的东西。
  • history实际采用了HTML5中提供的API来实现,主要有history.pushState()history.replaceState()