深入前端面试题

195 阅读39分钟

一、JS

1. 节流和防抖

(1) 是什么

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(节流)和debounce(防抖)的方式来减少调用频率。

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

image.png

image.png

(2) 区别

相同点:

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

不同点:

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

(3) 应用场景

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

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

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

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

(1) 基本数据 和 引用数据

  • 基本数据类型: 只包括自身一般存储在栈内存中,任何方法都不能修改基本数据类型本身的值,都是返回了一个新的。

  • 引用数据类型: 包括指针和内容,指针存储在栈内存,内容存储在堆内存。

(2) JS的内存空间 JS的内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。

其中栈存放变量,堆存放复杂对象,池存放常量,所以也叫常量池。

  • 基本数据类型保存在 栈内存 中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。

  • 引用数据类型存储在 堆内存 中,因为引用数据类型占据空间大、大小不固定。

image.png

3 类型判断

(1) typeof

typeof 对于基本类型而言,可以正常检测其类型;

对于引用类型,只能具体判断函数的类型为 function;

typeof null 的值为 object,这是因为一个久远的 Bug。如果想具体判断 null 类型的话直接 xxx === null 即可。

image.png

(1.2) null

  • undefined 的含义是 未被赋值 ;如果一个变量已经被声明,但未被赋值,那么它的值就是 undefined。

image.png

  • JS中的 null 仅仅是一个代表 无 、 空 或 值未知的 特殊值。
  • null 表示空,它是空对象
  • 当需要对数组销毁、对象销毁,或者删除时间监听,通常将他们设置为 null
box.onclick = null
typeof null       // "object"
let b = null
b == null   // true
b === null  // true

(1.3) NaN

  • 不是一个数,是一个数字类型的值
typeof NaN // "number"
  • 0除以0的结果是NaN;在数学运算中,若结果不能得到数字,其结果往往都是NaN。
0 / 0 // NaN '哈' - 3 // NaN 4 / 0 // Infinity
  • NaN不自等
NaN == NaN // false
  • 类型判断 isxxx API image.png

(2) Array.isArray()方法检测数组

var a = [1,2,3];
Array.isArray(a)		// true
Array.isArray([1,2,378])	// true
Array.isArray({a:1,d:55})	// false

(3) instanceof

  • 虽然 typeof 运算符在检测基本类型的时候非常好用,但检测引用类型的时候,就不行了。

通常并不想知道它是不是对象,而是想知道它到底是什么类型的对象,因为 数组是 object ,null 也是 object ,正则也是 object 。

这时应该用 instanceof 运算符来检测

instanceof 内部通过原型链的方式来判断是否为构建函数的实例,常用于判断具体的对象类型。

image.png

  • 当使用 instanceof 检查基本类型的值时,它会放回 false

image.png

(4) Object.prototype.toString

image.png

4 深拷贝浅拷贝

(1) 拷贝类型为基本类型

如果属性是基本类型,拷贝的就是基本类型的值。

如果属性是引用类型,拷贝的就是内存地址

(2) 拷贝类型为引用类型

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

(3) 浅拷贝

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

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

(3.1)浅拷贝—— Object.assign

Object.assign(dest,[src1,src2,src3...])

dest:目标对象

src1,src2,src3...:源对象

将所有源对象的属性拷贝到目标对象dest中。即从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中

调用结果返回dest

用Object.assign 替代 for .. in 循环来进行简单克隆,它将 user 中的所有属性拷贝到一个空对象中,并返回这个新的对象。

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);

(3.2)浅拷贝—— slice()

slice()方法返回一个含有被提取元素的新数组,原数组不会被改变

arr.slice(begin,end)

begin :可选;提取起始处的索引(从0开始),从该索引开始提取原数组元素

end :可选;提取终止处的索引(从0开始)。slice会提取原数组中索引从 begin 到 end 的所有元素( 包括 begin,但不包括 end )

image.png

// 浅拷贝
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"]

(3.3)浅拷贝—— concat()

concat()方法,合并2个或多个数组,此方法不会改变原数组,返回新的数组

var new_array = old_array.concat(value1 [ ,value2 [ ,...[ valueN ] ] ])

valueN :可选。将数组和/或值连接成新的数组

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

(3.4)浅拷贝—— ### 拓展运算符

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

(4) 深拷贝

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

常见的深拷贝方式有:

  • JSON.stringify()
  • 手写循环递归

(4.1) 深拷贝—— JSON.stringify

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

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

const obj = {
    name'A',
    name1undefined,
    name3function() {},
    name4:  Symbol('A')
}

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

console.log(obj2);       // {name: "A"}

JSON.parse(JSON.stringify(null))  // null

(4.2) 深拷贝—— 手写循环递归

function deepFun(obj) {
  if(typeof obj !== 'object' || obj == null) {
    return obj
  }

  let result
  if(obj instanceof Array) {
    result = []
  } else {
    result = {}
  }

  for(let key in obj) {
    if(obj.hasOwnProperty(key)) {
      result[key] = deepFun(obj[key])
    }
  }

  return result
}
5 类型转换

(1) 常见的类型转换有:

  • 强制转换(显示转换)
  • 自动转换(隐式转换)

(2) 显示转换

显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有:

  • Number()
  • parseInt()
  • String()
  • Boolean()

Number()

Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN

image.png

Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

Number(NaN)  // NaN

parseInt()

parseInt相比Number,就没那么严格了,parseInt函数逐个解析字符,遇到不能转换的字符就停下来

parseInt('32a3') //32

String()

可以将任意类型的值转化成字符串

// 数值:转为相应的字符串
String(1) // "1"

//字符串:转换后还是原来的值
String("a") // "a"

//布尔值:true转为字符串"true",false转为字符串"false"
String(true) // "true"

//undefined:转为字符串"undefined"
String(undefined) // "undefined"

//null:转为字符串"null"
String(null) // "null"

//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

Boolean()

可以将任意类型的值转为布尔值

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

(3) 隐式转换

在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换?

我们这里可以归纳为两种情况发生隐式转换的场景:

  • 比较运算(==!=><)、ifwhile需要布尔值地方
  • 算术运算(+-*/%

除了上面的场景,还要求运算符两边的操作数不是同一类型

自动转换为布尔值

在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数

可以得出个小结:

  • undefined
  • null
  • false
  • +0
  • -0
  • NaN
  • ""

除了上面几种会被转化成false,其他都换被转化成true

自动转换成字符串

遇到预期为字符串的地方,就会将非字符串的值自动转为字符串

具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串

常发生在+运算中,一旦存在字符串,则会进行字符串拼接操作

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

自动转换成数值

除了+有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

null转为数值时,值为0 。undefined转为数值时,值为NaN

6 使用 class 实现继承

(1.1) 实现继承

image.png

class Student {
    constructor(name,age) {
        this.name = name
        this.age = age
    }
    fun() {
        console.log(`我是${this.name},年龄是${this.age}`)
    }
}
const xiaoming = new Student('xiaomiing',20)
console.log('xiaoming',xiaoming)
xiaoming.fun()


class Teacher {
    constructor(name,subject) {
        this.name = name
        this.subject = subject
    }
    fun() {
        console.log(`我是${this.name},教的课程是${this.subject}`)
    }
}
const xiaoha = new Teacher('xiaoha','前端')
console.log('xiaoha',xiaoha)
xiaoha.fun()

(1.2) 相同的代码进行整合

在类继承的时候,不仅可以继承属性,也可以继承方法,extend;

super一定要写在最前面

image.png

class Person{
    constructor(name) {
        this.name = name
    }
    drink() {
        console.log('aaaa')
    }
}

class Student extends Person {        // extends 继承 属性name
    constructor(name,age) {
        // this.name = name
        super(name)                 // super(name) super一定要写在最前面
        this.age = age
    }
    fun() {
        console.log(`我是${this.name},年龄是${this.age}`)
    }
}
const xiaoming = new Student('xiaomiing',20)
console.log('xiaoming',xiaoming)
xiaoming.fun()
xiaoming.drink()                // 继承Person 的方法drink()


class Teacher extends Person{         // extends 继承 属性name
    constructor(name,subject) {
        // this.name = name
        super(name)
        this.subject = subject
    }
    fun() {
        console.log(`我是${this.name},教的课程是${this.subject}`)
    }
}
const xiaoha = new Teacher('xiaoha','前端')
console.log('xiaoha',xiaoha)
xiaoha.fun()
xiaoha.drink()

(2) 原型链继承

  function Parent() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child() {
    this.type = 'child2';
  }
  Child.prototype = new Parent();
  console.log(new Child())

上面代码看似没问题,实际存在潜在问题

var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]

改变s1play属性,会发现s2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的

7 原型

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

image.png

8 原型链

当访问对象属性或方法时,

=> 首先从自身开始找,如果找不到;

=> 就往原型上去找,如果原型上还是找不到;

=> 就往原型的原型上去找;

这样一来就形成一条链式结构,把它称做原型链。

__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的

image.png

每个对象的__proto__都是指向它的构造函数的原型对象prototype

person1.__proto__ === Person.prototype

构造函数是一个函数对象,是通过 Function构造器产生的

Person.__proto__ === Function.prototype

原型对象本身是一个普通对象,而普通对象的构造函数都是Object

Person.prototype.__proto__ === Object.prototype

刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function构造产生的

Object.__proto__ === Function.prototype

Object的原型对象也有__proto__属性指向nullnull是原型链的顶端

Object.prototype.__proto__ === null
9 Javscript数组的常用方法

(1) 操作方法

数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会

(1.1 )增

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

  • push()
  • unshift()
  • splice()
  • concat() 不会对原数组产生影响

push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度

let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
console.log(colors) // ["red", "green"]

unshift() 在数组开头添加任意多个值,然后返回新的数组长度

let colors = new Array(); // 创建一个数组
let count = colors.unshift("red""green"); // 从数组开头推入两项
alert(count); // 2
console.log(colors) // ["red", "green"]

splice() 传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组。

splice(下标值,数量,添加值)

数量=0,无删除

数量=1,删除1个

let colors = ["red""green""blue"];
let removed = colors.splice(10"yellow""orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []

concat() 首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

let colors = ["red""green""blue"];
let colors2 = colors.concat("yellow", ["black""brown"]);
console.log(colors); // ["red""green","blue"]
console.log(colors2); // ["red""green""blue""yellow""black""brown"]

(1.2) 删

下面三种都会影响原数组,最后一项不影响原数组:

  • pop()
  • shift()
  • splice()
  • slice() 不影响原数组

pop() 方法用于删除数组的最后一项,同时减少数组的length 值,返回被删除的项

let colors = ["red""green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length// 1

shift()方法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项

let colors = ["red""green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length// 1

splice() 传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组

let colors = ["red""green""blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组

slice()用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组

slice(a,b)截取的子数组从下标a开始,到下标b(但不包括b)结束;

slice()如果不提供第二个参数,则表示从指定项开始,提取所有后续所有项作为子数组;

let colors = ["red""green""blue""yellow""purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(14);
console.log(colors)   // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow

(1.2) 改

即修改原来数组的内容,常用splice

传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组

splice 的总结:

splice(下标值,数量,添加值),会改变原数组

数量=0,无删除

数量=1,删除1个

  • splice()方法用来替换数组中的指定项。
var arr = ['a','b','c','d','e','f']
arr.splice(3,2,'F','G','Y')	// 下标为3开始,连续替换2项
console.log(arr)			//  ["a", "b", "c", "F", "G", "Y", "f"]
  • splice()方法用于在指定的位置插入新项。
var arr = ['a','b','c','d']
arr.splice(2,0,'F','G','Y') // 第二个参数为0,即不删除原数组的任何一项,在下标为2的位置添加添加新项
console.log(arr)		//  ["a", "b", "F", "G", "Y", "c", "d"]
  • splice()方法用于删除指定项。
var arr = ['a','b','c','d','e','f']
arr.splice(3,2)	// 没有第三个参数,即没有设置替换的新项,
console.log(arr)	// ["a", "b", "c", "f"]
  • splice()方法以数组形式返回被删除的项。
var arr = ['a','b','c','d','e','f']
var newArr = arr.splice(3,2)
console.log(newArr)	// ["d", "e"]

(1.3) 查

即查找元素,返回元素坐标或者元素值

  • indexOf()
  • includes()
  • find()

indexOf()返回要查找的元素在数组中的位置,如果没找到则返回-1

let numbers = [123454321];\
numbers.indexOf(4) // 3

includes()返回要查找的元素在数组中的位置,找到返回true,否则false

let numbers = [123454321];\
numbers.includes(4) // true

find()返回第一个匹配的元素

const people = [
    {
        name: "Matt",
        age: 27
    },
    {
        name: "Nicholas",
        age: 29
    }
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}

(2) 排序方法

数组有两个方法可以用来对元素重新排序:

  • reverse()
  • sort()

reverse()顾名思义,将数组元素方向排列

let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1

sort()对一个数组进行排序,[2,8,11,6],已升序排序输出,已降序排序输出 image.png

(3) 转换方法

join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串

let colors = ["red""green""blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

image.png

(4) 迭代方法 常用来迭代数组的方法(都不改变原数组)有如下:

  • some()
  • every()
  • forEach()
  • filter()
  • map() some()对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true
let numbers = [123454321];
let someResult = numbers.every((item, index, array) => item > 2);
console.log(someResult) // true

every()对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true

let numbers = [123454321];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false

forEach()对数组每一项都运行传入的函数,没有返回值

let numbers = [123454321];
numbers.forEach((item, index, array) => {
    // 执行某些操作
});

filter()对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回

let numbers = [123454321];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3

map()对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

let numbers = [123454321];\
let mapResult = numbers.map((item, index, array) => item * 2);\
console.log(mapResult) // 2,4,6,8,10,8,6,4,2
10Javscript字符串的常用方法

(1) 操作方法 字符串常用的操作方法归纳为增、删、改、查

(1.1) 增

这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作

除了常用+以及${}进行字符串拼接之外,还可通过concat

concat用于将一个或多个字符串拼接成一个新字符串

let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"

(1.2) 删 这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作

常见的有:

  • slice()
  • substr()
  • substring()

slice()

  • slice(a,b)方法方法从a开始到b结束(不包括b)的子串。
"我喜欢JS,我也喜欢HTML".slice(3,5) // "JS"
  • slice(a,b)中a可以是负数
"我喜欢JS,我也喜欢HTML".slice(-4,-1) // "HTM"
  • slice(a,b)中a必须小于b
"我喜欢JS,我也喜欢HTML".slice(5,2) // ""

substr()

  • substr(a,b)中,得到从a开始的长度为b的子串
"我喜欢JS,我也喜欢HTML".substr(3,2) // "JS"
  • substr(a,b)中,可以b省略,表示到字符串的结尾。
"我喜欢JS,我也喜欢HTML".substr(3) // "JS,我也喜欢HTML"
  • substr(a,b)中,a可以是负数,表示倒数位置。
"我喜欢JS,我也喜欢HTML".substr(-4,2) // "HT"

substring()

  • substring(a,b)方法从a开始到b结束(不包括b)的子串。
"我喜欢JS,我也喜欢HTML".substring(3,5)	// "JS"
"我喜欢JS,我也喜欢HTML".substring(10,14)	// "HTML"
  • substring(a,b)忽略第二个参数,返回的子串一直到字符串的结尾。
"我喜欢JS,我也喜欢HTML".substring(6) // "我也喜欢HTML"
  • substring(a,b)中a可以大于b,数字顺序将自动调整小数在前。
"我喜欢JS,我也喜欢HTML".substring(5,3) // "JS"

对比总结:

substring(a,b)和slice(a,b)功能基本一致,都是从a开始到b结束(不包括b)的子串,

区别一:substring(a,b)可以自动交换2个参数的位置,而slice(a,b)不行;

区别二:slice(a,b)的参数a可以是负数,而substring(a,b)不行。

substr(a,b)中参数b是子串长度,而不是位置编号。

(1.3) 改 这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作

常见的有:

  • trim()、trimLeft()、trimRight()
  • repeat()
  • padStart()、padEnd()
  • toLowerCase()、 toUpperCase()

trim()、trimLeft()、trimRight()

删除前、后或前后所有空格符,再返回新的字符串

let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"

repeat() 接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果

let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na

padStart()、padEnd() 复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

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

toLowerCase()、 toUpperCase() 大小写转化

let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"

(1.4) 查 除了通过索引的方式获取字符串的值,还可通过:

  • chatAt()
  • indexOf()
  • startWith()
  • includes()

chatAt() 返回给定索引位置的字符,由传给方法的整数参数指定

let message = "abcde";
console.log(message.charAt(2)); // "c"

indexOf() 从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )

let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4

startWith() includes() 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值

let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false

(2) 转换方法 split 把字符串按照指定的分割符,拆分成数组中的每一项

let str = "12+23+34"
let arr = str.split("+") // [12,23,34]

(3) 模板匹配方法

针对正则表达式,字符串设计了几个方法:

  • match()
  • search()
  • replace()

match() 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数组

let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches[0]); // "cat"

search() 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,找到则返回匹配索引,否则返回 -1

let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1

replace() 接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)

let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"
11 闭包

闭包:一个函数和它的周围状态的引用捆绑在一起的组合

函数可以访问到其他函数作用域当中的数据;

子函数可以访问其他函数作用域中的数据,称之为闭包

内存泄漏:子函数一直 引用 父函数 里的变量,可以一直访问,不会消耗。

(1)函数作为返回值

function test() {
  const a = 1
  return function () {
    console.log('a',a)
  }
}
const fn = test()
const a = 2
fn()     // a 1

(2) 函数作为参数

function test(fn) {
  const a = 1
  fn()
}
const a = 2
function fn() {
  console.log('a',a)
}
test(fn)   // a 2
12 作用域、作用域链

(1) 作用域

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合

我们一般将作用域分成:

  • 全局作用域
  • 函数作用域
  • 块级作用域

全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

// 全局变量
var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// 打印 'Hello World!'
greet();

函数作用域

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

块级作用域

ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

{
  // 块级作用域中的变量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

(2) 作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

13 Javascript中this

(1) this

this:当前环境下,当前对象的引用

如果这个函数是对象的属性,或者叫类方法,这时的this就是当前的对象;

如果这个是普通的函数,这时的 this 就是全局对象,即Window

image.png

(2) 通过常量改变 this 指针 image.png

(3) 箭头函数中的this

image.png

image.png

14 # bind、call、apply

callapplybind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

(1) apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入

改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

(2) call call方法的第一个参数也是this的指向,后面传入的是一个参数列表

apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.call(obj,1,2); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

同样的,当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

(3) bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)

改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj
fn(1,2) // this指向window

image.png

从上面可以看到,applycallbind三者的区别在于:

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行
15 new操作符到底做了什么

new操作符用于创建一个给定构造函数的实例对象

  • a.创建一个空对象
  • b. 把空对象的原型指向构造函数的原型
  • c.改变this指向
  • d.返回这个对象
16 Ajax

image.png

image.png

17 JavaScript中本地存储的方式有哪些?

javaScript本地缓存的方法我们主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB

(1) cookie

Cookie,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP无状态导致的问题

作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie有效期、安全性、使用范围的可选属性组成

但是cookie在每次请求中都会被发送到服务器,如果不使用 HTTPS并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 cookie保持登录态的网站上,如果 cookie被窃取,他人很容易利用你的 cookie来假扮成你登录网站

  • Cookie 的基本用法 写入cookie

不能一起设置,只能一个一个设置: document.cookie="name=aaa;age=13" //这样是错误的

document.cookie="name=aaa" 
document.cookie="age=13"

读取cookie

读取的是一个由名值对构成的字符串,每个名值对之间由“;”(一个分号和一个空格)隔开

console.log(document.cookie) // name=aaa; age=13
  • Cookie 的属性1——失效时间

对于失效的Cookie,会被浏览器清除;

对于没有设置失效的Cookie,称为活Cookie,当会话结束,也就是浏览器关闭时,Cookie消失。

长时间存在,需要设置 Expires 或 Max-Age

Expires 值为 Date 类型

document.cookie="name=aaa;expires=${new Date('2100-1-01 00:00:00')}" 

Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比Expires高);

Max-Age 值为数字,表示当前时间 + 多少秒后过期,单位是秒;

如果 Max-Age 的值为0 或是负数,则 Cookie 会被删除。

document.cookie="name=aaa;max-age=5" 
  • Cookie 的属性2——Domain 域

Domain指定了 Cookie 可以送达的主机名

只能读写当前的父域的 Cookie,无法读写其他域的 Cookie

document.cookie="name=aaa;domain=www.imooc.com" 
  • Cookie 的属性3——Path 路径

Path 路径 限定了访问 Cookie 的 范围 (同一个域名下)

只能读写当前路径 和上级路径的 Cookie,无法读写下级路径的 Cookie

当 Name、Domain、Path 这3个字段都相同的时候,才是同一个 Cookie

document.cookie="name=aaa;path=/course/list" 
  • Cookie 的属性4——HttpOnly

设置了 HttpOnly 属性的 Cookie 不能通过JS去访问

  • Cookie 的属性5——Secure Secure 限定了 只有 在使用 https 而不是 http 的情况下才可以发送到服务器。

(2) localStorage

(2.1)特点

  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的
  • 存储的信息在同一域中是共享的
  • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
  • 大小:5M(跟浏览器厂商有关系)
  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
  • 受同源策略的限制
  • 它只是储存在本地,不会发送到服务器端。

(2.2)localStorage的基本用法:

1)设置setItem() ;2)获取 getItem() ;3)删除removeItem() ;4)一键删除全部clear()

//1)设置setItem()
localStorage.setItem('username','alex')

//有长度 length
console.log(localStorage.length)

//获取 getItem()
console.log(localStorage.getItem('username'))

//获取不存在的返回 null
console.log(localStorage.getItem('ftyj')) // null

//删除removeItem()
localStorage.removeItem('username')

//删除不存在的key,不报错
localStorage.removeItem('8952vfjjvj')

//一键删除全部clear()
 localStorage.clear()

(2.3)localStorage 也不是完美的,它有两个缺点:

  • 无法像Cookie一样设置过期时间
  • localStorage存储的键和值只能是字符串类型;不是字符串类型,也会自动先转换为字符串再存进去。
localStorage.setItem({},18)
console.log(typeof localStorage.getItem('[object Object]'))
console.log(localStorage.getItem('[object Object]'))
console.log({}.toString())

(3) sessionStorage

sessionStorage和 localStorage使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据

(4) 区别

关于cookiesessionStoragelocalStorage三者的区别主要如下:

  • 存储大小:cookie数据大小不能超过4ksessionStoragelocalStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大

  • 有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭

  • 数据与服务器之间的交互方式,cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端;sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保存

(5) 应用场景

  • 标记用户与跟踪用户行为的情况,推荐使用cookie
  • 适合长期保存在本地的数据(令牌),推荐使用localStorage
  • 敏感账号一次性登录,推荐使用sessionStorage
  • 存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用indexedDB
18 DOM

文档对象模型 (DOM) 是 HTML 和 XML 文档的编程接口

下面就来分析DOM常见的操作,主要分为:

  • 创建节点
  • 查询节点
  • 更新节点
  • 添加节点
  • 删除节点

(1) 创建节点 createElement

createElement: 创建新元素,接受一个参数,即要创建元素的标签名

注意事项:

  • 新创建出的节点是“孤儿节点”,这意味着它并没有被挂载到DOM树上,我们无法看到它。

  • 必须继续使用 appendChild() 或 insertBefore() 方法将孤儿节点插入DOM数上。

appendChild() 追加: 任何已经在DOM树上的节点,都可以调用appendChild()方法,它可以将孤儿节点挂载到它的内部,成为它的最后一个子节点。

<div id="box1">
    <p>我是原本的段落0</p>
    <p>我是原本的段落1</p>
    <p>我是原本的段落2</p>
</div>
<script>
    var box = document.getElementById('box1')
    // 创建节点,孤儿节点
    var op = document.createElement('p')
    // 设置内部文字
    op.innerText = "我是新来的段落"
    // 上树
    box.appendChild(op)
</script>

insertBefore(): 任何已经在DOM树上的节点,都可以调用insertBefore()方法,它可以将孤儿节点挂载到它的内部,成为它的“标杆子节点”之前的节点。

父节点.insertBefore(孤儿节点,标杆子节点 )

<div id="box1">
    <p>我是原本的段落0</p>
    <p>我是原本的段落1</p>
    <p>我是原本的段落2</p>
</div>
<script>
    var box = document.getElementById('box1')
    var p = document.getElementsByTagName('p')
    
    // 创建节点,孤儿节点
    var op = document.createElement('p')
    // 设置内部文字
    op.innerText = "我是新来的段落"
    
    // 上树
    box.insertBefore(op,p[1])
</script>

(2) 查询节点

image.png

querySelector

  • 传入任何有效的css 选择器,即可选中单个 DOM元素(首个);
  • 如果页面上没有指定的元素时,返回 null
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

querySelectorAll

  • 返回一个包含节点子树内所有与之相匹配的Element节点列表,如果没有相匹配的,则返回一个空节点列表

  • 需要注意的是,该方法返回的是一个 NodeList的静态实例,它是一个静态的“快照”,而非“实时”的查询

const notLive = document.querySelectorAll("p");

(3) 更新节点

innerHTML不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树

// 获取<p id="p">...</p>
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p>
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的内部结构已修改

innerText、textContent自动对字符串进行HTML编码,保证无法设置任何HTML标签;

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本;

innerHTML属性能以HTML语法设置节点中的内容

innerText属性只能以纯文本的形式设置节点中的内容

// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

style

DOM节点的style属性对应所有的CSS,可以直接获取或设置。遇到-需要转化为驼峰命名

// 获取<p id="p-id">...</p>
const p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // 驼峰命名
p.style.paddingTop = '2em';

(4) 删除节点

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉;

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置

// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentElement;
// 删除:
const removed = parent.removeChild(self);
removed === self; // true
19 事件的传播

实际上,事件的传播:先从外到内,然后再从内到外。先捕获阶段,再冒泡阶段

(1) onxxx写法只能监听到冒泡阶段

// 运行后的结果是:从内到外的顺序,冒泡阶段
// box3事件
// box2事件
// box1事件
<div id="box1">
    <div id="box2">
        <div id="box3"></div>
    </div>
</div>
<script>
    var box1 = document.getElementById('box1')
    var box2 = document.getElementById('box2')
    var box3 = document.getElementById('box3')
    
    box1.onclick = function(){
        console.log("box1事件")
    }
    box2.onclick = function(){
        console.log("box2事件")
    }
    box3.onclick = function(){
        console.log("box3事件")
    }
</script>

(2) DOM2级事件监听:addEventListener

DOM0级事件监听,只能监听冒泡阶段。

box.onclick = function(){}

DOM2级事件监听:addEventListener

box.addEventListener('click',function(){},true)

addEventListener的参数 true表示监听捕获阶段,false表示监听冒泡阶段。

// 运行后的结果是:从外到内,捕获阶段
// box1事件
// box2事件
// box3事件
<div id="box1">
    <div id="box2">
        <div id="box3"></div>
    </div>
</div>
<script>
    var box1 = document.getElementById('box1')
    var box2 = document.getElementById('box2')
    var box3 = document.getElementById('box3')
    
    box1.addEventListener('click',function(){console.log("box1事件")},true)
    box2.addEventListener('click',function(){console.log("box2事件")},true)
    box3.addEventListener('click',function(){console.log("box3事件")},true)
</script>

(3)注意事项:

最内部的元素不再区分捕获和冒泡阶段,会先执行写在前面的监听,然后执行后写的监听。

如果给元素设置相同或多个同名事件,则 DOM0 级写法后面写的会覆盖先写的DOM2 级会按顺序执行

// 允许结果:依次弹出bb、C、DD
<div id="box1">
    <div id="box2">
        <div id="box3"></div>
    </div>
</div>
<script>
    var box1 = document.getElementById('box1')
    var box2 = document.getElementById('box2')
    var box3 = document.getElementById('box3')
    
    box3.onclick = function() {
        alert("a")
    }
    box3.onclick = function() {
        alert("bb")
    }
    
    box3.addEventListener('click',function(){alert("C")},false)
    box3.addEventListener('click',function(){alert("DD")},false)
</script>
20 web常见的攻击方式

Web攻击(WebAttack)是针对用户上网行为或网站服务器等设备进行攻击的行为

如植入恶意代码,修改网站权限,获取网站用户隐私信息等等

我们常见的Web攻击方式有

  • XSS (Cross Site Scripting) 跨站脚本攻击
  • CSRF(Cross-site request forgery)跨站请求伪造
  • SQL注入攻击

(1.1) XSS

XSS,跨站脚本攻击,允许攻击者将恶意代码植入到提供给其它用户使用的页面中

XSS涉及到三方,即攻击者、客户端与Web应用

XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互

(1.2) XSS 预防

XSS攻击的两大要素:

  • 攻击者提交而恶意代码
  • 浏览器执行恶意代码

针对第一个要素,我们在用户输入的过程中,过滤掉用户输入的恶劣代码,然后提交给后端,但是如果攻击者绕开前端请求,直接构造请求就不能预防了

而如果在后端写入数据库前,对输入进行过滤,然后把内容给前端,但是这个内容在不同地方就会有不同显示

二、ES6

1 var、let、const之间的区别

(1) var

  • 在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变量

注意:顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象

var a = 10;
console.log(window.a// 10
  • 使用var声明的变量存在变量提升的情况
console.log(a) // undefined
var a = 20

在编译阶段,编译器会将其变成以下执行

var a
console.log(a)
a = 20
  • 使用var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
var a = 20 
var a = 30
console.log(a) // 30
  • 在函数中使用使用var声明变量时候,该变量是局部的
var a = 20
function change(){
    var a = 30
}
change()
console.log(a) // 20

如果在函数内不使用var,该变量是全局

var a = 20
function change(){
   a = 30
}
change()
console.log(a) // 30

(2) let

letES6新增的命令,用来声明变量

  • 用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效
{
    let a = 20
}
console.log(a) // ReferenceError: a is not defined.
  • 不存在变量提升

这表示在声明它之前,变量a是不存在的,这时如果用到它,就会抛出一个错误

console.log(a) // 报错ReferenceError
let a = 2
  • 使用let声明变量前,该变量都不可用,也就是大家常说的“暂时性死区

只要块级作用域内存在let命令,这个区域就不再受外部影响

var a = 123
if (true) {
    a = 'abc' // ReferenceError
    let a;
}
  • let不允许在相同作用域中重复声明
let a = 20
let a = 30
// Uncaught SyntaxError: Identifier 'a' has already been declared

注意的是相同作用域,下面这种情况是不会报错的

let a = 20
{
    let a = 30
}

因此,我们不能在函数内部重新声明参数

function func(arg) {
  let arg;
}
func()
// Uncaught SyntaxError: Identifier 'arg' has already been declared

(3) const

  • const声明一个只读的常量,一旦声明,常量的值就不能改变
const a = 1
a = 3
// TypeError: Assignment to constant variable.
  • 这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值
const a;
// SyntaxError: Missing initializer in const declaration
  • 如果之前用varlet声明过变量,再用const声明同样会报错
var a = 20
let b = 20
const a = 30
const b = 30
// 都会报错
  • const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动

对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量

对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

其它情况,constlet一致

(4) 区别

varletconst三者区别可以围绕下面五点展开:

  • 变量提升
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量
  • 使用

(4.1) 变量提升

var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined

letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错

(4.2) 暂时性死区

var不存在暂时性死区

letconst存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

(4.3) 块级作用域

var不存在块级作用域

letconst存在块级作用域

(4.4) 复声明变量

var允许重复声明变量

letconst在同一作用域不允许重复声明变量

(4.5) 修改声明的变量

varlet可以

const声明一个只读的常量。一旦声明,常量的值就不能改变

(4.6)使用

能用const的情况尽量使用const,其他情况下大多数使用let,避免使用var

2 Promise,async和await

(1) Promise状态

promise对象仅有三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

Promise特点

  • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
  • 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果

Promise用法

Promise对象是一个构造函数,用来生成Promise实例

const promise = new Promise(function(resolve, reject) {});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”

(2) 这个方式的缺点是: 代码的耦合程度太高,connected 和 disconnected 这两个函数的名称是固定的,而且直接写在 callSteven() 函数里面

image.png

(3) 虽然这种写法解耦了,但是可读性差

image.png

(4) 接下来改用 Promise 写法

.then 、 .catch() 写法 image.png

(5) async、await

try 里面是 resolve()的结果;

catch 里面 是 reject() 的结果

  try {

  } catch (e) {
    
  }

image.png

await 这个关键词的代码,只能在一个定义为 async的函数内执行

  async function action(params) {
    try {

    } catch (e) {
      
    }
  }

  action()

最后代码:

  function callSteven(success) {
    return new Promise((resolve,reject) => {
      console.log('Calling')
      setTimeout(() => {
        if(success) {
          resolve()
        }else {
          reject()
        }
      }, 3000)
    })
  }


  async function action(params) {
    try {
      await callSteven(true)
      console.log('Steven pick up the phone')
    } catch (e) {
      console.log('Steven reject the phone')
    }
  }

  action()

改为IIFE的写法,匿名函数然后立即执行

  (async () => {
    try {
      await callSteven(true)
      console.log('Steven pick up the phone')
    } catch (e) {
      console.log('Steven reject the phone')
    }
  })()
3 ES6中新增的Set

Setes6新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合;

集合:是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合;

集合是以 [值,值] 的形式存储元素

Set本身是一个构造函数,用来生成 Set 数据结构

const s = new Set();

(1) 增删改查

Set的实例关于增删改查的方法:

  • add()
  • delete()
  • has()
  • clear()

add() 添加某个值,返回 Set 结构本身; 当添加实例中已经存在的元素,set不会进行处理添加

s.add(1).add(2).add(2); // 2只被添加了一次

delete()删除某个值,返回一个布尔值,表示删除是否成功

s.delete(1)

has()返回一个布尔值,判断该值是否为Set的成员

s.has(2)

clear()清除所有成员,没有返回值

s.clear()

(2) 遍历

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

image.png

(3)扩展运算符和Set 结构相结合实现数组或字符串去重

// 数组
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)]; // [3, 5, 2]

// 字符串
let str = "352255";
let unique = [...new Set(str)].join(""); // '352'

(4) 类型转换

image.png

(5)交集 & 并集 & 差集

image.png

4 WeakSet

(1) WeakSet 语法

创建WeakSet实例

const ws = new WeakSet();

image.png

WeackSet只能成员只能是引用类型,而不能是其他类型的值

let ws=new WeakSet();

// 成员不是引用类型
let weakSet=new WeakSet([2,3]);
console.log(weakSet) // 报错

// 成员为引用类型
let obj1={name:1}
let obj2={name:1}
let ws=new WeakSet([obj1,obj2]); 
console.log(ws) //WeakSet {{…}, {…}}

(2) 在APIWeakSetSet有两个区别

  • 没有遍历操作的API
  • 没有size属性

(3) 引用类型垃圾回收原理

image.png

(4) WeakSet弱引用特性

image.png

weakSet是弱引用,如果这个地址被垃圾回收了,那么weakSet是没有值的

image.png

5 # ES6中新增的 Map

Map是一种叫做字典的数据结构

字典: 是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同

字典是以 [键,值] 的形式存储

Map类型是键值对的有序列表,而键和值都可以是任意类型

Map本身是一个构造函数,用来生成 Map 数据结构

const m = new Map()

(1) 增删改查

  • size 属性
  • set()
  • get()
  • has()
  • delete()
  • clear()

(1.1) size 属性返回 Map 结构的成员总数

const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2

(1.2) set()设置键名key对应的键值为value,然后返回整个 Map 结构

如果key已经有值,则键值会被更新,否则就新生成该键

同时返回的是当前Map对象,可采用链式写法

const m = new Map();

m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined
m.set(1, 'a').set(2, 'b').set(3, 'c') // 链式操作

(1.3) get方法读取key对应的键值,如果找不到key,返回undefined

const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello)  // Hello ES6!

(1.4) has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中

const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true

(1.5) delete方法删除某个键,返回true。如果删除失败,返回false

const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false

(1.6) clear方法清除所有成员,没有返回值

let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

(2) 遍历

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回所有成员的遍历器
  • forEach():遍历 Map 的所有成员

image.png

(3) Map类型转换

image.png

image.png

6 WeakMap

(1) WeakMap结构与Map结构类似,也是用于生成键值对的集合

APIWeakMapMap有两个区别:

  • 没有遍历操作的API
  • 没有clear清空方法

(2) WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名

image.png

(3) WeakMap是弱引用类型和前面的WeakSet一样

image.png

三、Css

1 CSS盒子模型

(1) 盒子模型

一个盒子由四个部分组成:content(width+height)paddingbordermargin

CSS中,盒子模型可以分成:

  • W3C 标准盒子模型
  • IE 怪异盒子模型

默认情况下,盒子模型为W3C 标准盒子模型

(1) 标准盒子模型

  • 标准盒子模型,是浏览器默认的盒子模型

  • 标准盒子模型 = margin、border、padding、content

  • content(width+height) 只是内容宽高,不包含 padding 和 border

  • content = width + height

(2) IE 怪异盒子模型

  • 怪异盒子模型 也是 = margin、border、padding、content

  • 标准 W3C 盒子模型不同的是:IE 盒子模型的 content 部分包含了 border 和 pading。

  • content = width + height + border + pading

(3) box-sizing

  • CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度

  • content-box 默认值,元素的 width/height 不包含padding,border,与标准盒子模型表现一致

  • border-box 元素的 width/height 包含 padding,border,与怪异盒子模型表现一致

  • inherit 指定 box-sizing 属性的值,应该从父元素继承

2
3
4
5
6

四、vue

1. vue2响应式原理 Object.defineProperty

(1) defineProperty: 定义属性

Object.defineProperty 声明一个属性

     function defineProperty() {
        var _obj = {};
        Object.defineProperty(_obj, "a", {
          value: 1,
        });
        return _obj;
      }
      var obj = defineProperty();
      console.log(obj);                    // { a: 1 }

Object.defineProperties 声明多个属性

     function defineProperty() {
        var _obj = {};
        Object.defineProperties(_obj,{
            a: {
                value: 11
            },
            b: {
                value: 22
            }
        })
        return _obj;
      }
      var obj = defineProperty();
      console.log(obj);               // { a: 11, b: 22 }

(2) Object.defineProperties 默认的增加了属性后,所输出的这个对象是不可修改,不可枚举,不可删除

  var _obj = {};
  Object.defineProperties(_obj, {
    a: {
      value: 11,
    },
    b: {
      value: 22,
    },
  });
  console.log(_obj)        // {a: 11, b: 22}
  
  _obj.a = 888
  console.log(_obj)       // {a: 11, b: 22}  属性值不可修改
  
  for(let i in _obj){
    console.log(i + ':' + _obj[i])   // 属性不可枚举 
                                     // 枚举: 把对象的每一项列举出来
                                     // 不可枚举: 不能拿到到key值
  }
  
  delete _obj.a
  console.log(_obj)      // {a: 11, b: 22}  属性不可删除

writable => 写; enumerable => 枚举; configurable => 可配置 , 可操作

  var _obj = {};
  Object.defineProperties(_obj, {
    a: {
      value: 11,
      writable: true,
      enumerable:true,
      configurable:true
    },
    b: {
      value: 22,
    },
  });
  console.log(_obj)       // {a: 11, b: 22}
  
  _obj.a = 888
  console.log(_obj)       // {a: 888, b: 22}
  
  for(let i in _obj){
    console.log(i + ':' + _obj[i])   // a:888
  }
  
  delete _obj.a
  console.log(_obj)      // {b: 22}

(3) 每个属性定义的时候,都会产生 getter 和 setter 的机制。

数据劫持 : 对待一个对象,它的取值或设置值,有一系列的配置和阻止的方法,按getter和setter的机制去进行数据操作的逻辑,数据在javaScript当中往往就是对象。

输出顺序: get => 1111

    const obj = {}
    let a = 1111
    Object.defineProperty(obj,'a',{
      get: function() {
        console.log('get')
        return a
      }
    })
    console.log(obj.a)   

输出顺序: get => 1111 => set => get => bbb

    const obj = {}
    let a = 1111
    Object.defineProperty(obj,'a',{
      get: function() {
        console.log('get')
        return a
      },
      set: function(newValue) {
        console.log('set')
        a = newValue  
        //视图重新渲染
      }
    })
    console.log(obj.a)
    
    obj.a = 'bbb'
    console.log(obj.a)

value 、writable 和 getter 、setter 不能同时一起设置,这两者是互斥的。

(4) 处理不同类型的情况

处理值为字符串的情况

输出: 更新视图层

  const data = {
    name: 'aa',
    age: 18
  }
  
  tyoeFn(data)
  
  function tyoeFn(target) {
    for(let key in target) {
      defineFn(target,key,target[key])
    }
  }
  
  function defineFn(target,key,value) {
    Object.defineProperty(target,key,{
      get: function() {
        return value
      },
      set: function(newValue) {
        if(value != newValue) {
          console.log('更新视图层')
          value = newValue
        }
      }
    })
  }
  
  data.name = 'bb'

处理值为复杂对象的情况

输出: 更新视图层

  const data = {
    name: 'aa',
    age: 18,
    obj: {
      nameObj: 'nameObj'
    }
  }
  
  tyoeFn(data)

  function tyoeFn(target) {
    if(typeof target !== 'object' || target === null) {
      return target
    }

    for(let key in target) {
      defineFn(target,key,target[key])
    }
  }
  function defineFn(target,key,value) {
  
    tyoeFn(value)
    
    Object.defineProperty(target,key,{
      get: function() {
        return value
      },
      set: function(newValue) {
        tyoeFn(newValue)
        if(value != newValue) {
          console.log('更新视图层')
          value = newValue
        }
      }
    })
  }
  
  data.obj.nameObj = 'bb'

Object.defineProperty:需要进行深度监听,才能够在数据改变的时候视图进行更新。

第一个缺点(深度监听):如果说这个数据是个对象,并且层级很深的话,一直不断的深度监听下去,直到是一个普通的值为止。如果数据很复杂的话,页面初次渲染会卡死。

第二个缺点:不能处理删除,新增,所以在vue中 删除用Vue.delete,新增用 Vue.set

(3) 处理值为数组的情况

用原型链给 Object.defineProperty里的 数组 增加push等方法

  const data = {
    arr:[1,2,3,5]
  }

  const oldArr = Array.prototype;
  const newArr = Object.create(oldArr);
  ['push','pop','shift','unshift'].forEach(methodName => {
    newArr[methodName] = function() {
      console.log('更新视图层')
      oldArr[methodName].call(this,...arguments)
    }
  })

  tyoeFn(data)

  function tyoeFn(target) {
    if(Array.isArray(target)) {
      target.__proto__ = newArr      //
    }

    if(typeof target !== 'object' || target === null) {
      return target
    }

    for(let key in target) {
      defineFn(target,key,target[key])
    }
  }
  function defineFn(target,key,value) {
    tyoeFn(value)
    Object.defineProperty(target,key,{
      get: function() {
        return value
      },
      set: function(newValue) {
        if(value != newValue) {
          console.log('更新视图层')
          value = newValue
        }
      }
    })
  }
  data.arr.push(666)
2. vue3响应式原理Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

proxy是es6新特性,为了对目标的作用主要是通过handler对象中的拦截方法拦截目标对象target的某些行为(如属性查找、赋值、枚举、函数调用等)。

target 目标对象 进行处理的对象; handler 容器 无数操作对象属性的方法

Proxy(target,handler)

target 操作的对象;prop 对象的属性;value: 对象属性值

get: (target, prop) set: (target, prop, value)

  let tar = {
    a:1,
    b:2
  }
  let proxy = new Proxy(tar, {
    get: function(tar, prop) {
      return '属性值' + tar[prop]
    },
    set: function(tar, prop,value) {
      tar[prop] = value
      console.log(tar[prop])
    }
  })
  console.log(proxy.a)   // 属性值1    访问proxy,就会走 get, 就会执行get里的方法
  console.log(tar.a)    // 1
  proxy.b = 3           // 3

总结:

(1) 通过proxy对象来代理target,先去操作proxy,然后通过handel重写proxy里面这些对对象操作的方式,来间接的操作target

(2)defineProperty,原则上是给对象增加属性用的,它在修改数组的长度,用所引去设置元素的值,数组的push,pop这些方法是无法触发defineProperty的setter方法

缺点:vue里使用的对数组的操作全都是vue自己重修写的,不是js的原生,所以导致代码非常重。

proxy没有以上问题,对数组的操作完全可以触发set方法。

(3)vue2没有使用proxy,因为proxy是es6的方法,因为兼容性的问题。vue3.0时,es6广泛推崇,很多浏览器es6兼容,所以使用proxy更加合理。proxy在vue在编译的时候,会进行转换,转换成es5相关执行,就是defineProperty。

(4) vue2.0的data是一个函数,用return返回的原因: 一是,防止引用值重写; 二是,产生的对象,是defineProperty直接操作这个对象。

vue3.0的data:{} 原因是: proxy做一层代理去访问,而这个对象target是必须通过代理proxy才能去完成一系列的data的操作

3. vue2.0 深入响应式原理——为啥数据更改,但页面没有响应更改?

是因为数据没有被依赖收集

数组 => 不会依赖收集,不管有没有值;

对象 => 空值不会依赖收集,有值才会依赖收集。

Vue2.0 不能检测数组和对象的变化。

(1)1. 对于对象

vue初始化实例时执行getter 和 setter 的转化,所以 prototy 必须在 data 上才能 让 vue 把它转换为响应式。

解决方法:Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

1.1 => 对于已经创建的实例 obj ,Vue 不允许动态添加根级别的响应式proterty; 即不存在的值(vue无法能检测proterty的添加或删除)。

    export default {
      data() {
        return {
          obj: { a: 1 },
        };
      },
      methods: {
        btn() {
          console.log(this.obj.a);    // 1
          console.log(this.obj.b);   // undefined

          //解决方法:Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
          this.$set(this.obj, "b", 2);
          console.log(this.obj.b); // 2
        },
      },
    };

1.2 => 空值不会依赖收集( obj 没有 a ,a 是事后添加的,此时虽然 obj有a ,但 Vue并没有响应式 a ,所以页面上 obj 显示 仍未 {} );

<p>{{obj}}</p>                            // 页面显示 {}
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
      obj: {}
    }
  },
  methods:{
    btn() {
      this.obj.a = 111
      console.log(this.obj.a)    // 111
    }
  }
}

1.3 => 有值才会依赖收集 ( obj 有 b ,b 是 在 data 里事前的,已被 Vue 依赖收集 转换响应式 )。

<p>{{obj}}</p>                           // 页面显示  { 'b': 33 }
<button @click="btn">按钮</button>
 
export default {
  name'Home',
  data() {
    return {
      obj: {b:22}
    }
  },
  methods:{
    btn() {
      this.obj.b = 33
      console.dir(this.obj.b)    // 33
    }
  }
}

1.4 => 重新更新 b 的依赖,vue 会遍历 obj ,在遍历的时候 把 a 依赖收集

<p>{{obj}}</p>                        // 页面显示 {"b": 33,"a": 555}
<button @click="btn">按钮</button>

export default {
  name: 'Home',
  data() {
    return {
      obj: {b:22}
    }
  },
  methods:{
    btn() {
      this.obj.b = 33
      this.obj.a = 555
      console.dir(this.obj)    // {"b": 33,"a": 555}
    }
  }
}

1.5 => 先点击 按钮22 ,再点击 按钮11,页面显示 {"b": 33,"a": 555} ;

这是因为 先点击 按钮22 ,此时 obj 里已经有了 a;

再点击 按钮11,重新更新 b 的依赖,vue 会遍历 obj ,在遍历的时候 把 a 依赖收集。

反过来,先点击 按钮 11,再点击 按钮22,页面显示 {"b": 33} 。

<p>{{obj}}</p>
<button @click="btn11">按钮11</button>
<button @click="btn22">按钮22</button>

export default {
  name'Home',
  data() {
    return {
      obj: {b:22}
    }
  },
  methods:{
    btn11() {
      this.obj.b = 33
    },
    btn22() {
       this.obj.a = 555
    }
  }
}

(2) 对于数组

Vue 不能检测以下数组的变动:

=> 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

=> 当你修改数组的长度时,例如:vm.items.length = newLength

解决办法 :

Vue.set => vm.$set(vm.items, indexOfItem, newValue)

splice => vm.items.splice(newLength)

<p>{{arr}}</p>                        // 页面显示 [1,2,3,4,5]
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[1,2,3,4,5]
    }
  },
  methods:{
    btn() {
      this.arr[2] = 'aaa'
      console.dir(this.arr)        // arr:[1,2,"aaa",4,5]
    }
  }
}
<p>{{arr}}</p>                            // 页面显示 [1,2,3,4,5]
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[1,2,3,4,5]
    }
  },
  methods:{
    btn() {
      this.arr.length = 2        
      console.dir(this.arr)        //  [1,2]
    }
  }
}

不会依赖收集,不管有没有值。

<p>{{arr}}</p>                        // 页面显示  []
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[]
    }
  },
  methods:{
    btn() {
      this.arr[0] = '111'
      console.dir(this.arr)        // arr['111']
    }
  }
}
<p>{{arr}}</p>                            // 页面显示  [1,2,3]
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[1,2,3]
    }
  },
  methods:{
    btn() {
      this.arr[0] = 'aaa'
      console.dir(this.arr)        // arr['aaa',2,3]
    }
  }
}
4.vue 中传值的形式

父子,子父 之间传值:

父 传 子 => propx

子 传 父 => on/emit

父 传 子 => parent/parent/children 仅适用只有一个子组件

父 传 子 => ref 用 this.refs来标记子组件,从而获取子组件实例

父 传 子 ,爷 传 孙子 => provoid / inject

父 传 子 => .sync修饰符

(1) 父 传 子 => propx

image.png

image.png

(2) 子 传 父 => on/emit

父组件 自定义组件使用on监听派发出的事件

子组件 this.$emit('gx') 使用emit派发自定义事件: gx

image.png

(3) parent/parent/children

this.$parent 得到父组件实例;

this.$children 得到子组件实例,可能有多个,且不保证顺序 ;

这种仅适用 一个 子组件。

image.png

image.png

image.png

(4) ref

image.png

(5) 子孙后代组件传值

=> provoid / inject 兄弟俩需要一起使用

  • provoid 对象 | 返回一个对象的函数。
  • inject 字符串数组 | 对象。

image.png

image.png

(6) 使用.sync修饰符

直接在子组件修改 父组件传过来的值,虽然可以, 但是 控制台 会报错;vue 不建议这么做,因为会造成 属性修改来源不明。

image.png

.sync修饰符可以实现 子组件 与 父组件 的双向绑定,并且可以实现子组件同步修改父组件的值。

image.png

同级/兄弟组件传值:

同级/兄弟组件,它们没有直接的隶属关系,对于这类的组件传值,通常会使用vuex、EventBus、observable。

(1) Event Bus

image.png

EventBus,也叫事件总线或者中央事件总线。说到底,无非就是利用Vue本身自带的 onon、emit进行。 on接收数据的那个组件;on 接收数据的那个组件; emit 发送数据的那个组件

image.png

需要注意的是:销毁组件时记得移除$on绑定的事件,避免造成重复监听。

image.png

(2)Vuex 实现组件全局状态(数据)管理机制

频繁、大范围的数据共享 => vuex

使用vuex的优点:1.能够在vuex集中管理共享的数据,易于开发和后期维护;2.能够高效实现组件之间的数据共享;3.存储在vuex中的数据都是响应式的,能够实时保持数据与页面的同步。

vuex的主要核心概念:State、Mutation、Action、Getter

image.png

(2.1) State 提供唯一的 公共数据源,所有的共享数据放在State存储

2.1.1 组件访问State中数据的第一种方式: this.$store.state.全局数据名称

image.png

2.1.2 组件访问State中数据的第二种方式: image.png

(2.2) Mutation 用于变更Store中的数据

2.2.1 只能通过mutation变更Store数据,可集中监控所有数据变化;不可以在组件中直接操作Store中的数据。

image.png

2.2.2 this.$store.commit() 触发 mutation的第一种方式

image.png

2.2.3 mapMutations函数 触发 mutation的第二种方式

image.png

(2.3) Action 用于处理异步任务

this.$store.dispatch() 触发actions 的第一种方式 image.png

image.png

image.png

image.png

(2.4)Getter 不会修改Store里的数据,只包装数据

image.png

image.png

image.png

image.png

(2.5) 解决浏览器刷新后vuex数据消失

image.png

image.png

image.png

(3) observable

5 方法调用、computed、watch的区别

方法:页面数据每次重新渲染都会重新执行。性能消耗大,除非不希望有缓存的时候用。

computed:计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。

watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

除非不希望缓存,一般用方法; 一般来说需要依赖别的属性来动态获得值的时候可以使用computed; 对于监听到的值变化需要做到异步操作或开销较大的操作用 watch。

6 混入 mixin 抽离组件公共逻辑

组件和 mixin 的 data 和 事件重复时,就用 组件的 替换 mixin 的,没有重复就用 mixin的;

生命周期钩子重复时,两者是并存的, 先执行 mixin ,再执行 组件的 。 image.png

7 keep-alive

Vue会把组件渲染缓存起来,当再次回到已经渲染的组件,这个组件不会再重新执行渲染的过程,会从缓存里的结果最终显示这个组件。

(1) 属性

keep-alivevue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例

(2) 生命周期钩子 activateddeactivated

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > createdmounted > activated > ... ... > beforeRouteLeave > deactivated

  • 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

  • activated在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用

因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子beforeCreate created beforeMount mounted 都不会触发。

  • deactivated:组件被停用(离开路由)时调用

使用了keep-alive就不会调用beforeDestroy(组件销毁前钩子)和destroyed(组件销毁),因为组件没被销毁,被缓存起来了

这个钩子可以看作beforeDestroy的替代,如果你缓存了组件,要在组件销毁的的时候做一些事情,你可以放在这个钩子里。

(3) 那假设切换组件时需要数据请求呢?缓存的组件应该不会重新请求了吧

不会,缓冲的还是之前请求到的数据,可以监听路由的变化再对应的请求接口;

不会重新请求了,因为mounted钩子不会再执行~。

=>所以在使用 keep-alive 的时候,Vue 会给你额外提供两个钩子,activated 和 deactivated;所以要请求数据的话可以放在 activated 中~。

(4) 缓存后如何获取数据? 解决方案可以有以下两种:

  • beforeRouteEnter
  • actived

每次组件渲染的时候,都会执行beforeRouteEnter

beforeRouteEnter(to, from, next){\
    next(vm=>{\
        console.log(vm)\
        // 每次进入路由执行\
        vm.getData()  // 获取数据\
    })\
}

keep-alive缓存的组件被激活的时候,都会执行actived钩子

activated(){\
   this.getData() // 获取数据\
}

(5)v-show 与 keep-alive

状态频繁切换,也可以使用 v-show

v-show 是通过css样式 display:none 来显示与否;

整体结构简单,使用v-show;

组件比较复杂,代码比较多,使用专门的组件缓存keep-alive。

8 异步组件

当组件test 的代码提交非常大时,首次渲染时加载不必要的代码时,这样就会很大的影响性能

=> 解决办法:异步组件

image.png

在组件注册时使用异步组件加载,当组件test代码很大的时候,首次渲染时不去下载这么大的代码,而是到需要的时候再去下载,这样就可以极大的提升性能。

image.png

image.png

image.png

9. 动态组件

通过 component 的 is 属性,来切换不同的组件。

  • <component></component> 按名称访问全局和本地组件,可以动态绑定我们的组件,根据数据不同更换不同的组件,在这种情况下,它更像是一个容器。

  • :is 属性:is-bind的缩写,component标签中的is属性决定了当前采用的是那个组件。

image.png

10 插槽

(1) 插槽 <slot> ,可以实现内容的动态发布

有时需要在组件模板中定义大量重复的内容区域,可以使用插槽来避免重复

PS:其中<span>****</span> 我们不希望是静态不变的,而是动态内容。 image.png

image.png

(2) 这时,需要将动态内容的模板部分使用<slot>插槽来实现

image.png

(3)<slot> 插槽会获取到组件元素包含的内容进行渲染;没有插槽,组件元素内部无法被识别。

image.png

(4)如果动态内容有较多一样,只有少部分不同,可以设置插槽默认值。

image.png

11 具名插槽 v-slot:名字

为了实现更多的复杂布局,系统提供了具名插槽来实现调用机制。

image.png

image.png

image.png

具名插槽,除了使用指令 v-slot 来调用,还支持 #号 的简写方式。

image.png

12 作用域插槽

父子组件之间存在作用域,它们的模板内容只能在自己的作用域编辑;

作用域的问题导致使用插槽时,无法通过父组件区域之间访问子组件内容。

解决思路:1.子组件插槽 v-bind 把数据传出;2.父组件 v-slot 指令获取子组件插槽的数据。

image.png

简写方案:

<current-user>
    <template v-slot="slotProps">
        {{slotProps.user.age}}
    </template>
</current-user>

ES6写法,解构

<current-user>
    <template v-slot="{{user}}">
        {{user.age}}
    </template>
</current-user>
13 nextTick

下次DOM 更新循环结束之后执行延迟回调。

在修改数据之后立即使用这个方法,获取更新后的DOM 。

使用场景: 如果想要在修改数据后立刻得到更新后的 DOM 结构,可以使用 VUE.nextTick()

image.png

image.png

image.png

image.png

14 父子组件生命周期执行顺序

image.png

15 Vue2 与 Vue3 生命周期的区别

image.png

16 v-if 和 v-show 的区别

最大的区别在于:对于不显示在页面上的处理

v-if是直接不渲染该元素;

v-show是做了css样式,display:none来控制元素不显示。

=> 什么时候使用v-if,啥时使用v-show呢?

=> 元素在页面上渲染时,它的显示与隐藏是一次性决定的,后面不再改变的,使用v-if会更好,因为使用v-show它会把不需要显示的也渲染在DOM里面。

=> 如果元素是在刚开始渲染后,还会再频繁的切换,一会显示一会隐藏,使用v-show会更好,因为它只需要操作css样式即可;但使用v-if的话,它要去创建dom元素,销毁dom元素,那这样的话性能开销会更加大。

17 v-model

父组件传入一个参数道子组件;

子组件监听这个值的变化;

利用emit。通知父组件更新这个值

image.png

18 对 vue 的理解

Vue.js 是一款流行的JavaScript前端框架;Vue作者是尤雨溪。

Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。

(1) Vue核心特性: 数据驱动(MVVM)

  • Model:模型层,应用的数据及业务逻辑
  • View:视图层:应用的展示效果,各类UI组件,可以简单的理解为HTML页面
  • ViewModel:视图模型层,框架封装的核心,用来连接Model和View,是Model和View之间的通信桥梁

(2) 组件化 : 在Vue中每一个.vue文件都可以视为一个组件

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现

  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单

  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

(3) 指令系统 解释:指令 (Directives) 是带有 v- 前缀的特殊属性

作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

  • 常用的指令
    • 条件渲染指令 v-if
    • 列表渲染指令v-for
    • 属性绑定指令v-bind
    • 事件绑定指令v-on
    • 双向数据绑定指令v-model

没有指令之前我们是怎么做的?是不是先要获取到DOM然后在....干点啥

19 双向绑定的理解

(1) 什么是双向绑定

单向绑定 : 把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。

双向绑定 : 在单向绑定的基础上,用户更新了ViewModel的数据也自动被更新了,这种情况就是双向绑定。

(2) 双向绑定的原理是什么

Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM

这里的控制层的核心功能便是 “数据双向绑定” 。

理解ViewModel 它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据
20 Vue生命周期的理解

(1) 生命周期有哪些

Vue生命周期总共可以分为8个阶段创建前后, 载入前后, 更新前后, 销毁前销毁后,以及一些特殊场景的生命周期

image.png

(2)Vue生命周期流程图

61a2bebd6a0cf2d90a722ad6ca00eb42.png

具体分析

beforeCreate -> created

  • 初始化vue实例,进行数据观测

created

  • 完成数据观测,属性与方法的运算,watchevent事件回调的配置
  • 可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computedwatch完成数据计算
  • 此时vm.$el 并没有被创建

created -> beforeMount

  • 判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译
  • 优先级:render > template > outerHTML
  • vm.el获取到的是挂载DOM

beforeMount

  • 在此阶段可获取到vm.el
  • 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上

beforeMount -> mounted

  • 此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM

mounted

  • vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM

beforeUpdate

  • 更新的数据必须是被渲染在模板上的(eltemplaterender之一)
  • 此时view层还未更新
  • 若在beforeUpdate中再次修改数据,不会再次触发更新方法

updated

  • 完成view层的更新
  • 若在updated中再次修改数据,会再次触发更新方法(beforeUpdateupdated

beforeDestroy

  • 实例被销毁前调用,此时实例属性与方法仍可访问

destroyed

  • 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
  • 并不能清除DOM,仅仅销毁实例

(3) 使用场景分析

image.png

(4) 数据请求在created和mouted的区别

created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成

mounted是在页面dom节点渲染完毕之后就立刻执行的

触发时机上created是比mounted要更早的

两者相同点:都能拿到实例对象的属性和方法

讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况

建议:放在create生命周期当中

21 为什么要在列表组件中写 key,其作用是什么?

key的作用 : key是给每一个vnode的唯一id,也是diff的一种优化策略可以根据key,更准确, 更快的找到对应的vnode节点

如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed

22 Vue常用的修饰符有哪些?有什么应用场景?

(1) 修饰符

vue中修饰符分为以下五种:

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

(2) 修饰符的作用

(2.1) 表单修饰符

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

  • lazy 在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步 v-model.lazy="value"

  • trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤 v-model.trim="value"

  • number 自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值 v-model.number="age"

(2.2) 事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

  • stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
  • prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
  • self 只当在 event.target 是当前元素自身时触发处理函数; 将事件绑定在自身身上,相当于阻止事件冒泡
  • once 绑定了事件以后只能触发一次,第二次就不会触发
  • capture 使事件触发从包含这个元素的顶层开始往下触发

(2.3) 鼠标按钮修饰符 鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • left 左键点击
  • right 右键点击
  • middle 中键点击

(2.4) 键盘修饰符 键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的

  • 普通键(enter、tab、delete、space、esc、up...)
  • 系统修饰键(ctrl、alt、meta、shift...)
<input @keyup.enter="submit">

(2.5) v-bind修饰符

v-bind修饰符主要是为属性进行操作,用来分别有如下:

  • async
  • prop
  • camel

async: 能对props进行一个双向绑定

//父组件
<comp :myMessage.sync="bar"></comp> 

//子组件
this.$emit('update:myMessage',params);

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  • 将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的

props: 设置自定义标签属性,避免暴露数据,防止污染HTML结

<input id="uid" title="title1" value="1" :index.prop="index">

camel : 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox

<svg :viewBox="viewBox"></svg>
23 跨域是什么?Vue项目中你是如何解决跨域的呢?

(1) 跨域是什么

跨域本质是浏览器基于同源策略的一种安全手段

同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

所谓同源(即指在同一个域)具有以下三个相同点

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)

反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

(2)如何解决 解决跨域的方法有很多,下面列举了三种:

  • JSONP
  • CORS
  • Proxy

而在vue项目中,我们主要针对CORSProxy这两种方案进行展开

(3) CORS : 跨域资源共享

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

(4) Proxy 也称网络代理

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象。

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

vue.config.js文件,新增以下代码

amodule.exports = {
    devServer: {
        host: '127.0.0.1',
        port: 8084,
        open: true,  // vue项目启动时自动打开浏览器\
        proxy: {
            '/api': {  // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                target: "http://xxx.xxx.xx.xx:8080"//目标地址,一般是指后台服务器地址
                changeOrigin: true//是否跨域
                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'""代替
                    '^/api'"" 
                }
            }
        }
    }
}
24 Vue2 与 Vue3 的区别

Vue3 简要就是:

  • 利用新的语言特性(es6)
  • 解决架构问题

Vue3的新特性:

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用
25 过滤器

过滤器: 就是把一些不必要的东西过滤掉

Vue 允许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废弃filter

使用方法:vue中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

image.png