一、JS
1. 节流和防抖
(1) 是什么
本质上是优化高频率执行代码的一种手段
如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(节流)和debounce(防抖)的方式来减少调用频率。
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次执行
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
(2) 区别
相同点:
- 都可以通过使用
setTimeout实现 - 目的都是,降低回调执行频率。节省计算资源
不同点:
- 函数防抖,在一段连续操作结束后,处理回调,利用
clearTimeout和setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能 - 函数防抖关注一定时间连续触发的事件,只在最后执行一次;而函数节流一段时间内只执行一次
(3) 应用场景
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
2 变量类型
(1) 基本数据 和 引用数据
-
基本数据类型: 只包括自身一般存储在栈内存中,任何方法都不能修改基本数据类型本身的值,都是返回了一个新的。
-
引用数据类型: 包括指针和内容,指针存储在栈内存,内容存储在堆内存。
(2) JS的内存空间 JS的内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。
其中栈存放变量,堆存放复杂对象,池存放常量,所以也叫常量池。
-
基本数据类型保存在 栈内存 中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
-
引用数据类型存储在 堆内存 中,因为引用数据类型占据空间大、大小不固定。
3 类型判断
(1) typeof
typeof 对于基本类型而言,可以正常检测其类型;
对于引用类型,只能具体判断函数的类型为 function;
typeof null 的值为 object,这是因为一个久远的 Bug。如果想具体判断 null 类型的话直接 xxx === null 即可。
(1.2) null
- undefined 的含义是 未被赋值 ;如果一个变量已经被声明,但未被赋值,那么它的值就是 undefined。
- 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
(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 内部通过原型链的方式来判断是否为构建函数的实例,常用于判断具体的对象类型。
- 当使用 instanceof 检查基本类型的值时,它会放回 false
(4) Object.prototype.toString
4 深拷贝浅拷贝
(1) 拷贝类型为基本类型
如果属性是基本类型,拷贝的就是基本类型的值。
如果属性是引用类型,拷贝的就是内存地址
(2) 拷贝类型为引用类型
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
(3) 浅拷贝
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象
在JavaScript中,存在浅拷贝的现象有:
Object.assignArray.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 )
// 浅拷贝
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));
但是这种方式存在弊端,会忽略undefined、symbol和函数
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
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
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) 隐式转换
在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换?
我们这里可以归纳为两种情况发生隐式转换的场景:
- 比较运算(
==、!=、>、<)、if、while需要布尔值地方 - 算术运算(
+、-、*、/、%)
除了上面的场景,还要求运算符两边的操作数不是同一类型
自动转换为布尔值
在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用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) 实现继承
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一定要写在最前面
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]
改变s1的play属性,会发现s2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的
7 原型
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身
8 原型链
当访问对象属性或方法时,
=> 首先从自身开始找,如果找不到;
=> 就往原型上去找,如果原型上还是找不到;
=> 就往原型的原型上去找;
这样一来就形成一条链式结构,把它称做原型链。
__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的
每个对象的__proto__都是指向它的构造函数的原型对象prototype的
person1.__proto__ === Person.prototype
构造函数是一个函数对象,是通过 Function构造器产生的
Person.__proto__ === Function.prototype
原型对象本身是一个普通对象,而普通对象的构造函数都是Object
Person.prototype.__proto__ === Object.prototype
刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function构造产生的
Object.__proto__ === Function.prototype
Object的原型对象也有__proto__属性指向null,null是原型链的顶端
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(1, 0, "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(1, 4);
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 = [1, 2, 3, 4, 5, 4, 3, 2, 1];\
numbers.indexOf(4) // 3
includes()返回要查找的元素在数组中的位置,找到返回true,否则false
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];\
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],已升序排序输出,已降序排序输出
(3) 转换方法
join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
(4) 迭代方法 常用来迭代数组的方法(都不改变原数组)有如下:
- some()
- every()
- forEach()
- filter()
- map()
some()对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.every((item, index, array) => item > 2);
console.log(someResult) // true
every()对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false
forEach()对数组每一项都运行传入的函数,没有返回值
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});
filter()对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
map()对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];\
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引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量
{
// 块级作用域中的变量
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
(2) 通过常量改变 this 指针
(3) 箭头函数中的this
14 # bind、call、apply
call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的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
当第一个参数为null、undefined的时候,默认指向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
同样的,当第一个参数为null、undefined的时候,默认指向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
从上面可以看到,apply、call、bind三者的区别在于:
- 三者都可以改变函数的
this对象指向 - 三者第一个参数都是
this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window - 三者都可以传参,但是
apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入 bind是返回绑定this之后的函数,apply、call则是立即执行
15 new操作符到底做了什么
new操作符用于创建一个给定构造函数的实例对象
- a.创建一个空对象
- b. 把空对象的原型指向构造函数的原型
- c.改变this指向
- d.返回这个对象
16 Ajax
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) 区别
关于cookie、sessionStorage、localStorage三者的区别主要如下:
-
存储大小:
cookie数据大小不能超过4k,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大 -
有效时间:
localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭 -
数据与服务器之间的交互方式,
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端;sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存
(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) 查询节点
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"><script>alert("Hi")</script></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
let是ES6新增的命令,用来声明变量
- 用法类似于
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
- 如果之前用
var或let声明过变量,再用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
其它情况,const与let一致
(4) 区别
var、let、const三者区别可以围绕下面五点展开:
- 变量提升
- 暂时性死区
- 块级作用域
- 重复声明
- 修改声明的变量
- 使用
(4.1) 变量提升
var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
(4.2) 暂时性死区
var不存在暂时性死区
let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
(4.3) 块级作用域
var不存在块级作用域
let和const存在块级作用域
(4.4) 复声明变量
var允许重复声明变量
let和const在同一作用域不允许重复声明变量
(4.5) 修改声明的变量
var和let可以
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构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”
(2) 这个方式的缺点是: 代码的耦合程度太高,connected 和 disconnected 这两个函数的名称是固定的,而且直接写在 callSteven() 函数里面
(3) 虽然这种写法解耦了,但是可读性差
(4) 接下来改用 Promise 写法
.then 、 .catch() 写法
(5) async、await
try 里面是 resolve()的结果;
catch 里面 是 reject() 的结果
try {
} catch (e) {
}
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
Set是es6新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合;
集合:是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合;
集合是以 [值,值] 的形式存储元素
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():使用回调函数遍历每个成员
(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) 类型转换
(5)交集 & 并集 & 差集
4 WeakSet
(1) WeakSet 语法
创建WeakSet实例
const ws = new WeakSet();
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) 在API中WeakSet与Set有两个区别
- 没有遍历操作的
API - 没有
size属性
(3) 引用类型垃圾回收原理
(4) WeakSet弱引用特性
weakSet是弱引用,如果这个地址被垃圾回收了,那么weakSet是没有值的
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 的所有成员
(3) Map类型转换
6 WeakMap
(1) WeakMap结构与Map结构类似,也是用于生成键值对的集合
在API中WeakMap与Map有两个区别:
- 没有遍历操作的
API - 没有
clear清空方法
(2) WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
(3) WeakMap是弱引用类型和前面的WeakSet一样
三、Css
1 CSS盒子模型
(1) 盒子模型
一个盒子由四个部分组成:content(width+height)、padding、border、margin
在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
父 传 子 => children 仅适用只有一个子组件
父 传 子 => ref 用 this.refs来标记子组件,从而获取子组件实例
父 传 子 ,爷 传 孙子 => provoid / inject
父 传 子 => .sync修饰符
(1) 父 传 子 => propx
(2) 子 传 父 => on/emit
父组件 自定义组件使用on监听派发出的事件
子组件 this.$emit('gx') 使用emit派发自定义事件: gx
(3) children
this.$parent 得到父组件实例;
this.$children 得到子组件实例,可能有多个,且不保证顺序 ;
这种仅适用 一个 子组件。
(4) ref
(5) 子孙后代组件传值
=> provoid / inject 兄弟俩需要一起使用
- provoid 对象 | 返回一个对象的函数。
- inject 字符串数组 | 对象。
(6) 使用.sync修饰符
直接在子组件修改 父组件传过来的值,虽然可以, 但是 控制台 会报错;vue 不建议这么做,因为会造成 属性修改来源不明。
.sync修饰符可以实现 子组件 与 父组件 的双向绑定,并且可以实现子组件同步修改父组件的值。
同级/兄弟组件传值:
同级/兄弟组件,它们没有直接的隶属关系,对于这类的组件传值,通常会使用vuex、EventBus、observable。
(1) Event Bus
EventBus,也叫事件总线或者中央事件总线。说到底,无非就是利用Vue本身自带的 emit进行。 emit 发送数据的那个组件
需要注意的是:销毁组件时记得移除$on绑定的事件,避免造成重复监听。
(2)Vuex 实现组件全局状态(数据)管理机制
频繁、大范围的数据共享 => vuex
使用vuex的优点:1.能够在vuex集中管理共享的数据,易于开发和后期维护;2.能够高效实现组件之间的数据共享;3.存储在vuex中的数据都是响应式的,能够实时保持数据与页面的同步。
vuex的主要核心概念:State、Mutation、Action、Getter
(2.1) State 提供唯一的 公共数据源,所有的共享数据放在State存储
2.1.1 组件访问State中数据的第一种方式:
this.$store.state.全局数据名称
2.1.2 组件访问State中数据的第二种方式:
(2.2) Mutation 用于变更Store中的数据
2.2.1 只能通过mutation变更Store数据,可集中监控所有数据变化;不可以在组件中直接操作Store中的数据。
2.2.2 this.$store.commit() 触发 mutation的第一种方式
2.2.3 mapMutations函数 触发 mutation的第二种方式
(2.3) Action 用于处理异步任务
this.$store.dispatch() 触发actions 的第一种方式
(2.4)Getter 不会修改Store里的数据,只包装数据
(2.5) 解决浏览器刷新后vuex数据消失
(3) observable
5 方法调用、computed、watch的区别
方法:页面数据每次重新渲染都会重新执行。性能消耗大,除非不希望有缓存的时候用。
computed:计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
除非不希望缓存,一般用方法; 一般来说需要依赖别的属性来动态获得值的时候可以使用computed; 对于监听到的值变化需要做到异步操作或开销较大的操作用 watch。
6 混入 mixin 抽离组件公共逻辑
组件和 mixin 的 data 和 事件重复时,就用 组件的 替换 mixin 的,没有重复就用 mixin的;
当 生命周期钩子重复时,两者是并存的, 先执行 mixin ,再执行 组件的 。
7 keep-alive
Vue会把组件渲染缓存起来,当再次回到已经渲染的组件,这个组件不会再重新执行渲染的过程,会从缓存里的结果最终显示这个组件。
(1) 属性
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive可以设置以下props属性:
include- 字符串或正则表达式。只有名称匹配的组件会被缓存exclude- 字符串或正则表达式。任何名称匹配的组件都不会被缓存max- 数字。最多可以缓存多少组件实例
(2) 生命周期钩子 activated与deactivated
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated与deactivated):
-
首次进入组件时:
beforeRouteEnter>beforeCreate>created>mounted>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 的代码提交非常大时,首次渲染时加载不必要的代码时,这样就会很大的影响性能。
=> 解决办法:异步组件
在组件注册时使用异步组件加载,当组件test代码很大的时候,首次渲染时不去下载这么大的代码,而是到需要的时候再去下载,这样就可以极大的提升性能。
9. 动态组件
通过 component 的 is 属性,来切换不同的组件。
-
<component></component>按名称访问全局和本地组件,可以动态绑定我们的组件,根据数据不同更换不同的组件,在这种情况下,它更像是一个容器。 -
:is 属性:is-bind的缩写,component标签中的is属性决定了当前采用的是那个组件。
10 插槽
(1) 插槽 <slot> ,可以实现内容的动态发布;
有时需要在组件模板中定义大量重复的内容区域,可以使用插槽来避免重复。
PS:其中<span>****</span> 我们不希望是静态不变的,而是动态内容。
(2) 这时,需要将动态内容的模板部分使用<slot>插槽来实现
(3)<slot> 插槽会获取到组件元素包含的内容进行渲染;没有插槽,组件元素内部无法被识别。
(4)如果动态内容有较多一样,只有少部分不同,可以设置插槽默认值。
11 具名插槽 v-slot:名字
为了实现更多的复杂布局,系统提供了具名插槽来实现调用机制。
具名插槽,除了使用指令 v-slot 来调用,还支持 #号 的简写方式。
12 作用域插槽
父子组件之间存在作用域,它们的模板内容只能在自己的作用域编辑;
作用域的问题导致使用插槽时,无法通过父组件区域之间访问子组件内容。
解决思路:1.子组件插槽 v-bind 把数据传出;2.父组件 v-slot 指令获取子组件插槽的数据。
简写方案:
<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()
14 父子组件生命周期执行顺序
15 Vue2 与 Vue3 生命周期的区别
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。通知父组件更新这个值
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就会自动更新。
双向绑定 : 在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定。
(2) 双向绑定的原理是什么
Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。
理解ViewModel 它的主要职责就是:
- 数据变化后更新视图
- 视图变化后更新数据
20 Vue生命周期的理解
(1) 生命周期有哪些
Vue生命周期总共可以分为8个阶段:创建前后, 载入前后, 更新前后, 销毁前销毁后,以及一些特殊场景的生命周期
(2)Vue生命周期流程图
具体分析
beforeCreate -> created
- 初始化
vue实例,进行数据观测
created
- 完成数据观测,属性与方法的运算,
watch、event事件回调的配置 - 可调用
methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computed和watch完成数据计算 - 此时
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
- 更新的数据必须是被渲染在模板上的(
el、template、render之一) - 此时
view层还未更新 - 若在
beforeUpdate中再次修改数据,不会再次触发更新方法
updated
- 完成
view层的更新 - 若在
updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated)
beforeDestroy
- 实例被销毁前调用,此时实例属性与方法仍可访问
destroyed
- 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
- 并不能清除DOM,仅仅销毁实例
(3) 使用场景分析
(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) 键盘修饰符
键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的
- 普通键(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项目中,我们主要针对CORS或Proxy这两种方案进行展开
(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>