1、谈一谈你对原型链的理解。
1.1 js里所有的对象都有proto属性(对象,函数),指向构造该对象的构造函数的原型。
1.2 只有函数function才具有prototype属性。这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。原型对象也有一个属性,叫做constructor,这个属性包含了一个指针,指回原构造函数。
<script>
// 父类
function Parent(){
this.user = '123'
};
Parent.prototype.getUser= '124545'
function Egent(){};
Egent.prototype = Parent.prototype;
// 子类
function Children(...arg){
Parent.call( this, ...arg )
};
// 原型继承
Children.prototype = new Egent();
console.log( new Children() )
</script>
找Children的prototype就可以找到父级定义的getUser。
2、实现数组扁平化[1,[2,4],4,[3,6]]三种方法。
2.1 第一种 ES6方法
<script>
let arr = [1,[2,4],4,[3,6]]
console.log(arr.flat(Infinity))
</script>
2.2 第二种使用reduce的方式
<script>
let arr = [1, [2, 4], 4, [3, 6]]
function arrFlat(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? arrFlat(cur) : cur);
}, [])
}
console.log(arrFlat(arr))
</script>
3.3 第三种使用递归加循环的方式
<script>
let arr = [1, [2, 4], 4, [3, 6]]
function arrFlat(arr) {
let result = [];
arr.map((item, index) => {
if (Array.isArray(item)) {
result = result.concat(arrFlat(item));
} else {
result.push(item);
}
})
return result;
}
console.log(arrFlat(arr))
</script>
3.4 第四种将数组先变成字符串,再复原 toString()
(只适用于数组元素全是String类型或Number类型)
<script>
let arr = [1, [2, 4], 4, [3, 6]]
function arrFlat(arr) {
return arr.toString().split(',').map(item => +item);
}
console.log(arrFlat(arr))
</script>
3、闭包是什么?在项目中用过吗?用在哪?
3.1 由于在JS中,变量的作用域属于函数作用域,在函数执行后作用域就会被清理、内存也随之被收回,但是由于闭包时建立在一个函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会随之销毁,这时的子函数---也就是闭包,便拥有了访问上级作用域中的变量的权限,即使上级函数执行完后,作用域内的值也不会被销毁。
<script>
function outer(){
var a = '变量1'
function inner(){
return a
}
return inner()
}
console.log(outer())
</script>
函数嵌套函数!!!
3.2 用过
3.3 一个 Ajax 请求的成功回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。简而言之,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时,都有闭包的身影。
4、使用原型链实现继承。
继承的几种方式: 原型链继承、原型式继承、寄生式继承、组合式继承、寄生组合式继承
4.1 原型链继承
function SuperType () {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType () {
this.subproperty = false
}
// 继承了SuperType //
SubType.prototype = new SuperType()
SubType.protype.getSubValue = function () {
return this.subproperty
}
var instance = new SubType()
4.2 原型式继承
function object (o) {
function F(){}
F.prototype = o
return new F()
}
4.3 寄生式继承
function createAnother (original) {
var clone = object(original) // 通过调用函数创建一个新对象
clone.sayHi = function () { // 以某种方式来增强这个对象
alert("hi")
}
return clone // 返回这个对象
}
4.4 组合式继承
function SuperType (name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType (name, age) {
// 继承属性
SuperType.call(this, name) // 第二次调用SuperType()
this.age = age
}
//继承方法
SubType.prototype = new SuperType() // 第一次调用 SuperType()
Subtype.prototype.sayAge = function () {
alert(this.age)
}
4.5 寄生组合式继承
function inheritPrototype (subType, superType) {
var prototype = object(superType.prototype) // 创建对象
prototype.constructor = subType // 增强对象
subType.prototype = prototype // 指定对象
}
function SuperType (name) {
this.name = name
this.colors = {"red", "blue", "green"}
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType (name, age) {
SuperType.call(this, name)
this.age = age
}
5、什么是防抖和截流。
什么是防抖和节流?有什么区别?如何实现?
5.1 防抖:连续触发事件,只要不是最后一次触发,就不执行异步操作
<button>点我试试</button>
<script>
var btn = document.querySelector('button')
var timer = null
btn.onclick = function () {
// 每次执行把上一次定时器清除,第一次执行也会清除timer
clearTimeout(timer)
// 延时 0.5s 执行
timer = setTimeout(() => {
console.log('发送请求了。。。')
}, 500)
}
</script>
5.2 节流:第一次发生请求后,只要响应没回来,就不能发送第二次(定义了一个开关控制)
<button>点我试试</button>
<script>
var btn = document.querySelector('button')
// 节流阀 定义了一个开关
var flag = true
btn.onclick = function () {
// 如果节流阀是开启的,才会执行操作
if (flag) {
// 一旦执行 关闭节流阀
flag = false
console.log('发送请求')
setTimeout(() => {
// 请求成功后,在开启节流阀
flag = true
}, 1000)
}
}
</script>
5.3 防抖和节流区别:防抖只会触发最后一次事件,节流只有请求成功发生响应后才会触发下一次事件
6、浅拷贝和深拷贝。
最简单理解就是 浅拷贝 会改变原数组,而 深拷贝不会改变原数组。
//浅拷贝 改变新数组原数组会发生改变
let arr = [1, 3, {
username: ' kobe'
}];
let brr = [...arr]
brr[2].username = "zs"
console.log(brr, arr);
//深拷贝 改变新数组 原素组不会发生改变
let arr1 = JSON.parse(JSON.stringify(arr));
arr1[2].username = 'duncan';
console.log(arr, arr1)
7、call、bind和apply的区别。
call
call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表。当第一个参数为null、undefined的时候,默认指向window。
var arr = [1, 2, 3, 89, 46]
var max = Math.max.call(null, arr[0], arr[1], arr[2], arr[3], arr[4])//89
可以这么理解:
obj1.fn()
obj1.fn.call(obj1);
fn1()
fn1.call(null)
f1(f2)
f1.call(null,f2)
看一个例子:
var obj = {
message: 'My name is: '
}
function getName(firstName, lastName) {
console.log(this.message + firstName + ' ' + lastName)
}
getName.call(obj, 'Dot', 'Dolby')
apply
apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。
var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89
可以这么理解:
obj1.fn()
obj1.fn.apply(obj1);
fn1()
fn1.apply(null)
f1(f2)
f1.apply(null,f2)
是不是觉得和前面写的call用法很像,事实上apply 和 call 的用法几乎相同, 唯一的差别在于:当函数需要传递多个变量时, apply 可以接受一个数组作为参数输入, call 则是接受一系列的单独变量。
看一个例子:
var obj = {
message: 'My name is: '
}
function getName(firstName, lastName) {
console.log(this.message + firstName + ' ' + lastName)
}
getName.apply(obj, ['Dot', 'Dolby'])// My name is: Dot Dolby
可以看到,obj 是作为函数上下文的对象,函数 getName 中 this 指向了 obj 这个对象。参数 firstName 和 lastName 是放在数组中传入 getName 函数。
call和apply可用来借用别的对象的方法,这里以call()为例:
var Person1 = function () {
this.name = 'Dot';
}
var Person2 = function () {
this.getname = function () {
console.log(this.name);
}
Person1.call(this);
}
var person = new Person2();
person.getname(); // Dot
从上面我们看到,Person2 实例化出来的对象 person 通过 getname 方法拿到了 Person1 中的 name。因为在 Person2 中,Person1.call(this) 的作用就是使用 Person1 对象代替 this 对象,那么 Person2 就有了 Person1 中的所有属性和方法了,相当于 Person2 继承了 Person1 的属性和方法。
对于什么时候该用什么方法,其实不用纠结。如果你的参数本来就存在一个数组中,那自然就用 apply,如果参数比较散乱相互之间没什么关联,就用 call。像上面的找一组数中最大值的例子,当然是用apply合理。
bind
和call很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用。
bind返回值是函数
var obj = {
name: 'Dot'
}
function printName() {
console.log(this.name)
}
var dot = printName.bind(obj)
console.log(dot) // function () { … }
dot() // Dot
bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 printName 中的 this 并没有被改变,依旧指向全局对象 window。
参数的使用
function fn(a, b, c) {
console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');
fn('A', 'B', 'C'); // A B C
fn1('A', 'B', 'C'); // Dot A B
fn1('B', 'C'); // Dot B C
fn.call(null, 'Dot'); // Dot undefined undefined
call 是把第二个及以后的参数作为 fn 方法的实参传进去,而 fn1 方法的实参实则是在 bind 中参数的基础上再往后排。
有时候我们也用bind方法实现函数珂里化,以下是一个简单的示例:
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
在低版本浏览器没有 bind 方法,我们也可以自己实现一个。
if (!Function.prototype.bind) {
Function.prototype.bind = function () {
var self = this, // 保存原函数
context = [].shift.call(arguments), // 保存需要绑定的this上下文
args = [].slice.call(arguments); // 剩余的参数转为数组
return function () { // 返回一个新函数
self.apply(context, [].concat.call(args, [].slice.call(arguments)));
}
}
}
8、var、let和const的区别。
8.1、var声明的变量会挂载在window上,而let和const声明的变量不会:
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
8.2、var声明变量存在变量提升,let和const不存在变量提升
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined ===> 找不到c这个变量
const c = 10;
8.3、let和const声明形成块作用域
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
if(1){
var a = 100; const c = 1; }
console.log(a); // 100 console.log(c) // 报错:c is not defined ===> 找不到c这个变量
8.4、同一作用域下let和const不能声明同名变量,而var可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
let a = 100;
let a = 10;
// 控制台报错:Identifier 'a' has already been declared ===> 标识符a已经被声明了。
8.5、暂存死区
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
8.6、const
1、 一旦声明必须赋值,不能使用null占位。
2、 声明后不能再修改
3、 如果声明的是复合类型数据,可以修改其属性
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
9、JS中包含了哪些数据类型?
在JavaScript中每一个值都属于某一种数据类型。JavaScript的数据类型共有六种。它们分别是undefined、null、boolean、number、string、object
它们共分为两大类,分别为:
基本类型:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)
引用类型:对象(Object)、数组(Array)、函数(Function)
<script>
const string = 'abc123'
const number = 12345
const boolean = true //false
const nul = null
const undef = undefined
const object = {
name: '刘青林',
age: 22
}
const array = ['abc', 123, true, null, undefined]
const fun = function () {
return '刘青林真帅'
}
</script>
10、JS如何阻止事件冒泡?
如果没有阻止冒泡,点击黄色盒子会发生事件冒泡,让绿色和红色也触发。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.one {
width: 200px;
height: 200px;
background-color: red
}
.two {
width: 150px;
height: 150px;
background-color: green
}
.three {
width: 100px;
height: 100px;
background-color: yellow
}
</style>
</head>
<body>
<div class="one">
<div class="two">
<div class="three"></div>
</div>
</div>
</body>
</html>
<script>
let one = document.querySelector(".one")
let two = document.querySelector(".two")
let three = document.querySelector(".three")
one.addEventListener("click", (e) => {
//阻止事件冒泡
e.stopPropagation()
console.log('我是最外层')
})
two.addEventListener("click", (e) => {
//阻止事件冒泡
e.stopPropagation()
console.log('我是中间层')
})
three.addEventListener("click", (e) => {
//阻止事件冒泡
e.stopPropagation()
console.log('我是最里层')
})
</script>
11、箭头函数和普通函数之间的区别。
// 普通函数
function func(){
// code
}
// 箭头函数
let func=()=>{
// code
}
1.1 箭头函数都是匿名函数
普通函数可以有匿名函数,也可以有具体名函数,但是箭头函数都是匿名函数。
代码实例如下:
// 具名函数
function func(){
// code
}
// 匿名函数
let func=function(){
// code
}
// 箭头函数全都是匿名函数
let func=()=>{
// code
}
11.2 箭头函数不能用于构造函数,不能使用new
普通函数可以用于构造函数,以此创建对象实例。
代码实例如下:
function Person(name,age){
this.name=name;
this.age=age;
}
let admin=new Person("太阳是我种的",25);
console.log(admin.name);
console.log(admin.age);
Person用作构造函数,通过它可以创建实例化对象。
11.3 但是构造函数不能用作构造函数。
箭头函数中this的指向不同在普通函数中,this总是指向调用它的对象,如果用作构造函数,this指向创建的对象实例。
11.3.1 箭头函数本身不创建this
也可以说箭头函数本身没有this,但是它在声明时可以捕获其所在上下文的this供自己使用。
注意:this一旦被捕获,就不再发生变化
var webName="捕获成功";
let func=()=>{
console.log(this.webName);
}
func();
代码分析:
(1)wrap()用作构造函数。
(2)使用new调用wrap()函数之后,此函数作用域中的this指向创建的实例化对象。
(3)箭头函数此时被声明,捕获这个this。
(4)所以打印的是太阳是我种的2,而不是太阳是我种的1。
11.4 结合call(),apply()方法使用
箭头函数结合call(),apply()方法调用一个函数时,只传入一个参数对this没有影响。
let obj2 = {
a: 10,
b: function(n) {
let f = (n) => n + this.a;
return f(n);
},
c: function(n) {
let f = (n) => n + this.a;
let m = {
a: 20
};
return f.call(m,n);
}
};
console.log(obj2.b(1)); // 结果:11
console.log(obj2.c(1)); // 结果:11
11.5 箭头函数不绑定arguments,取而代之用rest参数…解决
每一个普通函数调用后都具有一个arguments对象,用来存储实际传递的参数。但是箭头函数并没有此对象。
function A(a){
console.log(arguments);
}
A(1,2,3,4,5,8); // [1, 2, 3, 4, 5, 8, callee: ƒ, Symbol(Symbol.iterator): ƒ]
let B = (b)=>{
console.log(arguments);
}
B(2,92,32,32); // Uncaught ReferenceError: arguments is not defined
let C = (...c) => {
console.log(c);
}
其他区别
(1)箭头函数不能Generator函数,不能使用yeild关键字。
(2)箭头函数不具有prototype原型对象。
(3)箭头函数不具有super。
(4)箭头函数不具有new.target。
总结:
(1).箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply()
(2).普通函数的this指向调用它的那个对象
12、普通函数内部的this走向。
12.1 普通函数
正常模式:this指向Window
function fn () {
console.log(this) //Window
}
fn();
严格模式:this不知道指向谁,所以打印undefined
function fn () {
'use strict'
console.log(this) //undefined
}
fn();
12.2 表达式函数
严格模式下this是undefined,正常模式是Window
let fn = function() {
console.log(this) //Window
}
fn();
12.3 构造函数
构造函数的this指向实例对象,下例指向实例对象fn。
function Fn(name) {
this.name=name;
console.log(this) //Fn {name: "constructor function"}
}
let fn = new Fn('constructor function');
12.4 对象方法调用
对象调用自身方法时的this指向该方法所属的对象
let obj = {
name:'zy',
sayHi:function() {
console.log(this)
}
}
obj.sayHi() //obj {name: "zy", sayHi: ƒ}
12.5 事件绑定方法
事件绑定时,this指向当前事件所绑定的对象
//html
<button>cilck</button>
//js
let btn = document.querySelector('button');
btn.onclick = function() {
console.log(this) //<button>点我</button>
}
12.6 定时器函数
定时器里的函数是回调函数,所有回调函数的this都指向Window
setTimeout(function(){
console.log(this) //Window
},1000)
12.7 立即执行函数(自调用函数)
严格模式下this是undefined,正常模式是Window
(function(){
'use strict'
console.log(this) //undefined
})()
13、cookie、localStroage和sessionStroage之间的区别和使用方法。
cookie
(1)HTTP Cookie简称cookie,在HTTP请求发送Set-Cookie HTTP头作为响应的一部分。通过name=value的形式存储
(2)cookie的构成:
名称:name(不区分大小写,但最好认为它是区分的)
值:value(通过URL编码:encodeURIComponent)
域
路径
失效时间:一般默认是浏览器关闭失效,可以自己设置失效时间
安全标志:设置安全标志后只有SSL连接的时候才发送到服务器
(3)cookie的作用:主要用于保存登录信息
(4)生命期为只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。 存放数据大小为4K左右 。有个数限制(各浏览器不同),一般不能超过20个。与服务器端通信:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
(5)cookie的优点:具有极高的扩展性和可用性
通过良好的编程,控制保存在cookie中的session对象的大小
通过加密和安全传输技术,减少cookie被破解的可能性
只有在cookie中存放不敏感的数据,即使被盗取也不会有很大的损失
控制cookie的生命期,使之不会永远有效。这样的话偷盗者很可能拿到的就 是一个过期的cookie
(6)cookie的缺点:
cookie的长度和数量的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉
安全性问题。如果cookie被人拦掉了,那个人就可以获取到所有session信息。加密的话也不起什么作用
有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务端保存一个计数器。若吧计数器保存在客户端,则起不到什么作用。
sessionStorage
(1) sessionStorage是Storage类型的一个对象,拥有,clear(),getItem(name),key(index),removeItem(name),setItem(name,value)方法
(2)sessionStorage对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭
(3)将数据保存在session对象中。所谓session,是指用户在浏览某个网站时,从进入网站到浏览器关闭所经过的这段时间,也就是用户浏览这个网站所花费的时间。session对象可以用来保存在这段时间内所要求保存的任何数据
(4)sessionStorage为临时保存
localStorage
(1)localStorage也是Storage类型的一个对象
(2)在HTML5中localStorage作为持久保存在客户端数据的方案取代了globalStorage(globalStorage必须指定域名
(3)localStorage会永久存储会话数据,除非removeItem,否则会话数据一直存在
(4)将数据保存在客户端本地的硬件设备(通常指硬盘,也可以是其他硬件设备)中,即使浏览器被关闭了,该数据仍然存在,下次打开浏览器访问网站时仍然可以继续使用
(5)localStorage为永久保存
cookie、session和localStorage的区别
(1)cookie的内容主要包括:名字、值、过期时间、路径和域,路径与域一起构成cookie的作用范围。若不设置时间,则表示这个cookie的生命期为浏览器会话期间,关闭浏览器窗口,cookie就会消失,这种生命期为浏览器会话期的cookie被称为会话cookie
(2)会话cookie一般不存储在硬盘而是保存在内存里,当然这个行为并不是规范规定的。若设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再打开浏览器这些cookie仍然有效直到超过设定的过期时间。对于保存在内存里的cookie,不同的浏览器有不同的处理方式session机制。
(3)当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识(称为session id),如果已包含则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个),如果客户端请求不包含session id,则为客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。
14、Object.assign的使用(请写完整)。
14.1 Object.assign()对象的拷贝
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 Object.assign(target, ...sources) 【target:目标对象】,【souce:源对象(可多个)】
举个栗子:
const object1 = {
a: 1,
b: 2,
c: 3
};
const object2 = Object.assign({c: 4, d: 5}, object1);
console.log(object2.c, object2.d);
console.log(object1) // { a: 1, b: 2, c: 3 }
console.log(object2) // { c: 3, d: 5, a: 1, b: 2 }
注意:
1.如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性
2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标
对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如
果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到
原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。
14.2 Object.assign()对象的深拷贝
针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}
obj2.a = 2;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}}
//最后一次赋值的时候,b是值是对象的引用,只要修改任意一个,其他的也会受影响
// Deep Clone (深拷贝)
obj1 = { a: 0 , b: { c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
14.3 对象的合并
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };
const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
其实就是对象的拷贝,o1就是目标对象,后面的是源对象,后面的属性等会拷贝到目标对象
14.4 合并具有相同属性的对象
const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };
const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
1.属性被后续参数中具有相同属性的其他对象覆盖。
2.目标对象的属性与源对象的属性相同,源的会覆盖目标的属性
14.5 继承属性和不可枚举属性是不能拷贝
const obj = Object.create({foo: 1}, { // foo 是个继承属性。
bar: {
value: 2 // bar 是个不可枚举属性。
},
baz: {
value: 3,
enumerable: true // baz 是个自身可枚举属性。
}
});
//创建对象时,如果没有设置enumerable的值,默认为false(不可枚举属性),设置为true,则为可枚举属性
const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
14.6 原始类型会被包装为对象
const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")
const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
14.7 异常会打断后续拷贝任务
const target = Object.defineProperty({}, "foo", {
value: 1,
writable: false
}); // target 的 foo 属性是个只读属性。
Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。
console.log(target.bar); // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo); // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz); // undefined,第三个源对象更是不会被拷贝到的。
15、Promise的使用。
Promise的作用以及基本使用
关于Promise的概念,在实际使用之前对其的理解一直比较模糊,只是停留在一些文档上的描述。在使用中其实可以根据其特性进行一些更佳的实践。在这里简单介绍一下其作用以及基础用法。
作用
Promise对象可以理解为一次执行的异步操作,使用promise对象之后可以使用一种链式调用的方式来组织代码;让代码更加的直观。也就是说,有了Promise对象,就可以将异步操作以同步的操作的流程表达出来,避免了层层嵌套的回调函数。总结一下就是可以将原先不可控的回调通过promise转为更加可控更清晰的方式表达,更加高效,更便于维护。
基本用法
1、首先我们new一个Promise,将Promise实例化
2、然后在实例化的promise可以传两个参数,一个是成功之后的resolve,一个是失败之后的reject
3、Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数
var promise = function(isReady){
return new Promise(function(resolve, reject){
// do somthing, maybe async
if (isReady){
return resolve('成功执行');
} else {
return reject('出错了');
}
});
}
//Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。
promise(true).then(function(value){
// success,这里是resolve的回调函数
console.log(value); //hello world
}, function(err){
// failure,这里是reject的回调函数
console.log(err)
})
上述代码是执行成功,返回成功执行,如果想测试一下失败后的返回值,可以把promise(true)这里改为 promise(false)在控制台试下
链式操作
Promise并不只是简化层层回调的写法,更重要的在于是通过传递状态的方式来使回调方式能够及时的调用,因此,相比于callback,它更灵活,更简单。下面我们来看看Promise的链式操作:
makePromise1()
.then(function(value){
console.log(value);
return makePromise2();
})
.then(function(value){
console.log(value);
return makePromise3();
})
.then(function(value){
console.log(value);
});
function makePromise1(){
var p = new Promise(function(resolve, reject){
//异步操作
setTimeout(function(){
console.log('异步1');
resolve('异步1参数');
}, 2000);
});
return p;
}
function makePromise2(){
var p = new Promise(function(resolve, reject){
//异步操作
setTimeout(function(){
console.log('异步2');
resolve('异步2参数');
}, 2000);
});
return p;
}
function makePromise3(){
var p = new Promise(function(resolve, reject){
//异步操作
setTimeout(function(){
console.log('异步3');
resolve('异步3参数');
}, 2000);
});
return p;
}
上面的代码中,有三个异步操作,makePromise1,makePromise2,makePromise3。其中第二个和第三个依次执行,也就是上一个操作完成之后才可以进行。会相继的打印出异步1,异步1参数···
Promise的catch方法
var promise = function(isReady){
return new Promise(function(resolve, reject){
if (isReady){
return resolve('成功执行');
} else {
return reject('失败');
}
});
}
promise(true)
.then(function(value){
console.log('resolved');
console.log(value);
console.log(wawa); //此处的wawa未定义
})
.catch(function(error){
console.log('rejected');
console.log(error);
});
catch 方法是 then(onFulfilled, onRejected) 方法当中 onRejected 函数的一个简单的写法,也就是说可以写成 then(fn).catch(fn),相当于 then(fn).then(null, fn)使用 catch 的写法比一般的写法更加清晰明确,其实可以类比成try/catch,这样,其中有报错的地方不会阻塞运行。比如定义了一个未定义wawa,正常来说它上面的代码也不会运行,因为被这个报错阻塞了,有了catch,它上面的代码可以正常运行下去
promise.all方法
var p1 = new Promise(function (resolve) {
setTimeout(function () {
resolve("第一个promise");
}, 3000);
});
var p2 = new Promise(function (resolve) {
setTimeout(function () {
resolve("第二个promise");
}, 1000);
});
Promise.all([p1, p2]).then(function (result) {
console.log(result); // ["第一个promise", "第二个promise"]
});
上面的代码中,all接收一个数组作为参数,p1,p2是并行执行的,等两个都执行完了,才会进入到then,all会把所有的结果放到一个数组中返回,所以我们打印出我们的结果为一个数组。值得注意的是,虽然p2的执行顺序比p1快,但是all会按照参数里面的数组顺序来返回结果。
promise.race方法
var p1 = new Promise(function (resolve) {
setTimeout(function () {
console.log(1);
resolve("第一个promise");
}, 3000);
});
var p2 = new Promise(function (resolve) {
setTimeout(function () {
console.log(2);
resolve("第二个promise");
}, 1000);
});
Promise.race([p1, p2]).then(function (result) {
console.log(result);
});
// 结果:
// 2
// 第二个promise
// 1
在这可以看到,传的值中,只有p2的返回了,但是p1没有停止,依然有执行。race的应用场景为,比如我们可以设置为网路请求超时。写两个promise,如果在一定的时间内如果成功的那个我们没有执行到,我们就执行失败的那个。
21、什么是同源策略?同源策略的三个限制是什么?有哪些html标签不被同源策略限制?
21.1 指: 同协议、端口、域名的安全策略,由网景(Netscape)公司提出来的安 全协议!
21.2
(1) 无法用js读取非同源的Cookie、LocalStorage 和 IndexDB 无法读取。
(2) 无法用js获取非同源的DOM 。
(3) 无法用js发送非同源的AJAX请求 。更准确的说,js可以向非同源的服务器发请求,但是服务器返回的数据会被浏览器拦截。
21.3
<script src="...">//加载图片到本地执行
<img src="..."> //图片
<link href="...">//css
<iframe src="...">//任意资源
22、常用的跨域方式有哪些?
-
利用jsonp进行跨域。(至于json与jsonp之间的区别,我在另一篇文章中描述过)这种方法就不用介绍了,这也是最常见的跨域方式之一。window.name+iframe window.name通过在iframe(一般动态创建i)中加载跨域HTML文件来起作用。然后,HTML文件将传递给请求者的字符串内容赋值给window.name。然后,请求者可以检索window.name值作为响应。
-
window.postMessage() HTML5新特性,可以用来向其他所有的 window 对象发送消息。需要注意的是我们必须要保证所有的脚本执行完才发送 MessageEvent,如果在函数执行的过程中调用了它,就会让后面的函数超时无法执行。
-
WebSocket WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很棒的实现。相关文章,请查看:WebSocket WebSocket-SockJS
-
图片ping或script标签跨域 图片ping常用于跟踪用户点击页面或动态广告曝光次数。script标签可以得到从其他来源数据,这也是JSONP依赖的根据。缺点:只能发送Get请求 ,无法访问服务器的响应文本(单向请求)
23、跨域proxy的原理是什么?jsonp的原理是什么?
- proxy 跨域原理实质上是利用 http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器。 例如:本地主机A为 http://localhost:3000 ,该主机浏览器发送一个请求,接口为 /api ,这个请求的数据(响应)在另外一台服务器B http://10.231.133.22:80 上,这时,就可以通过A主机设置webpack proxy,直接将请求发送给B主机。
- 动态创建script标签,回调函数。JSONP (JSON with Padding)是JSON的一种"使用模式",可用于解决主流浏览器的跨域数据访问的问题。. 利用
25、Promise和async/await之间的关系。
25.1 async
定义异步函数(内部通常有异步操作),返回Promise对象(函数返回Promise→显式返回return的Promise;函数返回非Promise→隐式返回Promise.resolve()包装的return值;)
25.2 await
- 只能放在async函数中,等待右侧表达式结果(函数→结果=return值;字符串→结果=字符串;)
- wait 等到了它要等的东西,一个 Promise 对象,或者其它值,如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
- 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
- 阻塞后面的代码,先执行async外部的同步代码,同步代码执行完再回到async内部,拿到运算结果(表达式返回Promise→等待Promise对象fulfilled,再将resolve参数作为表达式的运算结果;表达式返回非Promise→直接作为表达式的运算结果;)
25.3 区别
- 函数前面多了一个aync关键字。await关键字只能用在aync定义的函数内。async函数会隐式地返回一个promise,该promise的reosolve值就是函数return的值。(示例中reosolve值就是字符串”done”)
- 第1点暗示我们不能在最外层代码中使用await,因为不在async函数内。
26、ES6的symbol有什么用?
概念
- symbol 英文意思为 符号、象征、标记、记号,在 js 中更确切的翻译应该为 独一无二的值,symbol是ES6中新增的一种数据类型,被划分到了基本数据类型中。
- 基本数据类型:字符串、数字、undefined、布尔、null、symbol
- 引用数据类型:object
常用方法
- 创建一个symbol类型的值
const s = Symbol();
console.log(typeof s); // "symbol"
- 即使是传入相同的参数,生成的 symbol 值也是不相等的,因为 Symbol 本来就是独一无二的意思
const foo = Symbol('foo');
const bar = Symbol('foo');
console.log(foo === bar); // false
- Symbol.for 方法可以检测上下文中是否已经存在使用该方法且相同参数创建的 symbol 值,如果存在则返回已经存在的值,如果不存在则新建。
const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
console.log(s1 === s2); // true
- Symbol.keyFor 方法返回一个使用 Symbol.for 方法创建的 symbol 值的 key
const foo = Symbol.for("foo");
const key = Symbol.keyFor(foo);
console.log(key) // "foo"
- 同时关于Symbol 还有很多种内置的属性比如说,hasInstance、isConcatSpreadable、species、match、replace、search、spli、interator、toPrimitive、toStringTag、unscopables、等等
27、协商缓存和强缓存是什么?(请详细描述)
1、背景介绍
做前端有两个比较令人头痛的事,一个是命名,另一个就是缓存了。HTTP协议提供了非常强大的缓存机制, 了解这些缓存机制,对提高网站的性能非常有帮助。
2、知识剖析
什么是浏览器缓存
浏览器缓存(Brower Caching)是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。
浏览器是如何判断是否使用缓存的
浏览器缓存的优点有:
- 减少了冗余的数据传输,节省了网费
- 减少了服务器的负担,大大提升了网站的性能
- 加快了客户端加载网页的速度
浏览器缓存主要有两类:缓存协商和彻底缓存,也有称之为协商缓存和强缓存。
- 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码;
- 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;
两者的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求。
缓存中header的参数:
3、强制缓存
- Expires:response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存。
- Cache-Control: 当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
- cache-control除了该字段外,还有下面几个比较常用的设置值:
- -no-cache: 不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- -no-store: 直接禁止浏览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- -public: 可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- -private: 只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
协商缓存
Last-Modify/If-Modify-Since:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间;当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
- Etag: web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。
- If-None-Match: 当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定是否命中协商缓存;
ETag和Last-Modified的作用和用法,他们的区别:
- Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;
- 在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值;
- 在优先级上,服务器校验优先考虑Etag。
浏览器缓存过程
- 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
- 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求
- 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;;
- 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
3.常见问题
用户行为对浏览器缓存的影响
4.解决方案
- 点击刷新按钮或者按F5
- 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是304,也有可能是200.
- 用户按Ctrl+F5(强制刷新)
- 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是200.
- 地址栏回车
- 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
28、http状态码分为大致的几类?能列举其中常用的几种并说明其使用的意义吗?
1XX :信息状态码
100 Continue 继续,一般在发送 post 请求时,已发送了 http header 之后服务端将 返回此信息,表示确认,之后发送具体参数信息
2XX :成功状态码
200 OK 正常返回信息 201 Created 请求成功并且服务器创建了新的资源 202 Accepted 服务器已接受请求,但尚未处理
3XX :重定向
301 Moved Permanently 请求的网页已永久移动到新位置。
302 Found 临时性重定向。
303 See Other 临时性重定向,且总是使用 GET 请求新的 URI 。
304 Not Modified 自从上次请求后,请求的网页未修改过。
4XX :客户端错误
400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内 容发起请求。
401 Unauthorized 请求未授权。
403 Forbidden 禁止访问。
404 Not Found 找不到如何与 URI 相匹配的资源。
5XX: 服务器错误
500 Internal Server Error 最常见的服务器端错误。
503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。
29、什么是restful风格的接口?能谈谈你自己的理解吗?
含义:
restful是一种软件架构风格、设计风格、而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁、更有层次、更易于实现缓存等机制。
传统的接口用URL来描述行为,RESTful用url来描述资源,针对的是资源。
传统API就是在url中去定义行为,可以从路径中看出来这个API是用来做什么的,而RESTfulAPI的url是用来描述资源的,比如/user/1要描述的就是id=1的user是一个资源,假设我们的数据库中有100个user对象,就对应的是100个资源
使用HTTP方法来描述行为。使用HTTP状态码来表示不同的结果。
RESTful API是用HTTP的方法来描述行为,GET——请求表示查询,DELETE——请求表示删除,PUT——请求表示修改,POST——请求表示新增;传统的API接口,不论调用成功与否,返回的状态码可能都是200,只是在返回的数据中,有某个字段判断是否调用成功;而RESTfulAPI是通过HTTP状态码来表示不同的结果,比如:200——表示调用成功,400——表示调用失败,500——表示异常等。
使用json交互数据。
传统的API可能使用字符串拼接,可能使用xml等各种形式进行数据的交换;而在RESTful API中都是使用json进行数据的交互。
说白了,restful只是一种风格,并不是强制的标准。
31、手写一个bind方法。
Function.prototype.myBind = function(obj, ...args){
const that = this;
return function(...newArgs) {
return that.apply(obj,[...args,...newArgs])
}
}
var obj = {
username:'zhnagsan,
age:23,
}
function foo(name,age,school){
console.log(this)
console.log(name,age,school)
return 111
}
const returnVal = foo.myBind(obj,'zhangsan',23)('youxi')
console.log(returnVal)
32、ajax中的get方法和post方法有什么区别?
get请求是从指定的资源请求数据,get请求基本上用于从服务器获得(取回)数据。
注释:GET 方法可能返回缓存数据。
post请求是向指定的资源提交要处理的数据,post请求也可用于从服务器获取数据。不过,post方法不会缓存数据,并且常用于连同请求一起发送数据。
ajax中get请求和post请求的区别一:
get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。post是通过HTTP post机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程。
ajax中get请求和post请求的区别二:
对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。两种方式的参数都可以用Request来获得。
ajax中get请求和post请求的区别三:
get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,因服务器的不同而异。
ajax中get请求和post请求的区别四:
get安全性非常低,post安全性较高。
ajax中get请求和post请求的区别五:
跟是一样的,也就是说,method为get时action页面后边带的参数列表会被忽视;而跟是不一样的。
ajax中get请求和post请求的区别六:
Get请求有如下特性:它会将数据添加到URL中,通过这种方式传递到服务器,通常利用一个问号?代表URL地址的结尾与数据参数的开端,后面的参数每一个数据参数以“名称=值”的形式出现,参数与参数之间利用一个连接符&来区分。 Post请求有如下特性:数据是放在HTTP主体中的,其组织方式不只一种,有&连接方式,也有分割符方式,可隐藏参数,传递大批数据,比较方便。
post请求和get请求分别在什么情况下使用
当符合下列任一情况,则用post方法:
1、请求的结果有持续性的副作用,例如,数据库内添加新的数据行。
2、若使用GET方法,则表单上收集的数据可能让URL过长。
3、要传送的数据不是采用7位的ASCII编码。
当符合下列任一情况,则用get方法:
1、请求是为了查找资源,HTML表单数据仅用来帮助搜索。
2、请求结果无持续性的副作用。
3、收集的数据及HTML表单内的输入字段名称的总长不超过1024个字符。
33、什么是xss和csrf攻击?怎么防范?
XSS
简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中。
XSS 可以分为多种类型,但是总体上分为两类:持久型和非持久型。 持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站 访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。
对于 XSS 攻击来说,通常有两种方式可以用来防御。
33.1 转义字符
首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出 的内容,对于引号、尖括号、斜杠进行转义
function escape(str) {
str = str.replace(/&/g, '&')
str = str.replace(/</g, '<')
str = str.replace(/>/g, '>')
str = str.replace(/"/g, '&quto;')
str = str.replace(/'/g, ''')
str = str.replace(/`/g, '`')
str = str.replace(///g, '/')
return str
}
通过转义可以将攻击代码变成
// -> <script>alert(1)</script>
escape('<script>alert(1)</script>')
但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这 样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当 然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多, 更加推荐使用白名单的方式
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1><script>alert("xss");</script>
console.log(html)
以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤 了 script 标签
33.2 CSP
CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载 和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通 过这种方式来尽量减少 XSS 攻击。
通常可以通过两种方式来开启 CSP
- 设置 HTTP Header 中的 Content-Security-Policy
- 设置 meta 标签的方式 这里以设置 HTTP Header 来举例 只允许加载本站资源
这里以设置 HTTP Header 来举例
只允许加载本站资源
Content-Security-Policy: default-src 'self'
只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
允许加载任何来源框架
Content-Security-Policy: child-src 'none'
当然可以设置的属性远不止这些,对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞, 攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。
CSRF
中文名为跨站请求伪造。
原理就是攻击者构造出一个后端请求地址, 诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的 话,后端就以为是用户在操作,从而进行相应的逻辑。
举个例子,假设网站中有一个通过 GET 请求提交用户评论的接口,那么攻击者就可以在钓 鱼网站中加入一个图片,图片的地址就是评论接口
<img src="http://www.domain.com/xxx?comment='attack'"/>
那么你是否会想到使用 POST 方式提交请求是不是就没有这个问题了呢?其 实并不是,使用这种方式也不是百分百安全的,攻击者同样可以诱导用户进入 某个页面,在页面中通过表单提交 POST 请求。
如何防御CSRF
Get 请求不对数据进行修改
不让第三方网站访问到用户 Cookie
阻止第三方网站请求接口
请求时附带验证信息,比如验证码或者 Token
33.3 SameSite
可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请 求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览 器都兼容。
33.4 验证 Referer
对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是 否为第三方网站发起的。
33.5 Token
服务器下发一个随机 Token ,每次发起请求时将 Token 携带上,服务器验 证 Token 是否有效
34、js的垃圾回收机制是怎样的?
JS为什么需要垃圾回收机制程序的运行需要内存,只要程序提出要求,操作系统或者运行是就必须供给内存。对于持续运行的服务进程,必须及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
JS垃圾回收常用的两种方式
环境的定义:《JavaScript高级程序设计第三版》定义执行环境(为简单起见,有时也称环境)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们的各自行为。全局执行环境是最外围的一个执行环境,在Web浏览器中,全局执行环境被认为是window对象。全局执行环境被销毁时(例如关闭网页或者关闭浏览器),保存在内部的变量和函数也随之被销毁。每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将环境弹出,把控制权返回给之前的执行环境。
1. 标记清除
js中最常用的方式就是标记清除(mark-and-sweep)。当变量进入一个环境(例如在函数内声明一个变量)时,就将这个变量标记为 “进入环境”。因为从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入环境的时候进入相应的环境的时候,就有可能会用到这些变量。而当变量离开环境时,则将其标记为"离开环境"。
垃圾收集器会在运行的时候给存储在内存中所以的变量都加上标记,然后它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁哪些带标记的值并回收他们所占用的内存空间
2. 引用计数
引用计数策略并不常用。引用计数的含义是跟踪记录每个指被引用的次数。当声明了一个变量并将一个引用类型的值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给了另一个变量,则该值的引用次数加 1。相反,如果包含这个值的变量又引用了另外一个值,则这个值的引用次数就减 1。当这个值的引用次数变成 0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来了。这样当垃圾收集器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
35、什么是eventloop?
Eventloop 不是ECMAScript 标准,而是HTML 标准,各浏览器会有不同程度的执行
因为JavaScript 它是一种单线程语言,一个进程一次只能执行一个任务,等前面的任务执行完了,再执行后面的任务。所有任务都在一个线程上完成,一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现 “假死” ,因为JavaScript停不下来,也就无法响应用户的行为。
从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
什么是单线程
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。 如果前一个任务耗时很长,后一个任务就不得不一直等着。 js 引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。
- 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
- 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程
宏任务 微任务
为了解决这种情况,将任务分为了同步任务和异步任务;
而异步任务被分为两种,一种宏任务(MacroTask),一种叫微任务(MicroTask)
先执行宏任务再执行宏任务里面的微任务
什么是EventLoop
结束本次宏任务 检查还有没有宏任务需要处理 这个检查的过程是持续进行的 每完成一个任务都会进行一次 而这样的操作就被称为Event Loop。
Event Loop 即事件循环,是指浏览器或 Node 的一种解决 javaScript 单线程运行时不会阻塞的一种机制,也就是我们经常使用 异步 的原理。
36、谈一谈你对webpack的理解。
价值
我当时使用 webpack 的一个最主要原因是为了简化页面依赖的管理,并且通过将其打包为一个文件来降低页面加载时请求的资源数。
原理
它将所有的资源都看成是一个模块,并且把页面逻辑当作一个整体,通过一个给定的入口文件,webpack 从这个文件开始,找到所有的依赖文件,将各个依赖文件模块通过 loader 和 plugins 处理后,然后打包在一起,最后输出一个浏览器可识别的 JS 文件。
四个核心概念
1、Entry(入口) :webpack 的入口起点,它指示 webpack 应该从哪个模块开始着手,来作为其构建内部依赖图的开始。
2、Output(输出) :告诉 webpack 在哪里输出它所创建的打包文件,也可指定打包文件的名称,默认位置为 ./dist。
3、loader:webpack 的编译器,它使得 webpack 可以处理一些非 JavaScript 文件。在对 loader 进行配置的时候,test 属性,标志有哪些后缀的文件应该被处理,是一个正则表达式。use 属性,指定 test 类型的文件应该使用哪个 loader 进行预处理。常用的 loader 有 css-loader、style-loader 等。
4、Plugins(插件) :可以用于执行范围更广的任务,包括打包、优化、压缩、搭建服务器等等,要使用一个插件,一般是先使用 npm 包管理器进行安装,然后在配置文件中引入,最后将其实例化后传递给 plugins 数组属性。
优/缺点
webpack 的确能够提供我们对于项目的管理,但是它的缺点就是调试和配置起来太麻烦了。但现在 webpack4.0 的免配置一定程度上解决了这个问题。但是我感觉就是对我来说,就是一个黑盒,很多时候出现了问题,没有办法很好的定位。
37、什么是函数柯里化,函数柯里化的意义是什么?
函数柯里化又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数预置通用参数,供多次重复调用。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
add1(20) === 21
柯里化的特点
- 参数复用(固定易变因素)
- 延迟执行
- 提前返回
柯里化的缺点
柯里化是牺牲了部分性能来实现的,可能带来的性能损耗:
- 存取 arguments 对象要比存取命名参数要慢一些
- 老版本浏览器在 arguments.lengths 的实现相当慢(新版本浏览器忽略)
- fn.apply() 和 fn.call() 要比直接调用 fn() 慢
- 大量嵌套的作用域和闭包会带来开销,影响内存占用和作用域链查找速度
柯里化的应用
- 利用柯里化制定约束条件,管控触发机制
- 处理浏览器兼容(参数复用实现一次性判断)
- 函数节流防抖(延迟执行)
- ES5前bind方法的实现
为何要使用柯里化
函数柯里化是函数编程中的一个重要的基础,它为我们提供了一种编程的思维方式。显然,它让我们的函数处理变得复杂,代码调用方式并不直观,还加入了闭包,多层作用域嵌套,会有一些性能上的影响。
但在一些复杂的业务逻辑封装中,函数柯里化能够为我们提供更好的应对方案,让我们的函数更具自由度和灵活性。
38、将地址输入到浏览器地址栏里,到加载页面的整个过程是什么?
1、浏览器构建HTTP Request请求
2、网络传输
3、服务器构建HTTP Response 响应
4、网络传输
5、浏览器渲染页面
39、一个html页面的的渲染过程是怎样的?
浏览器接收到 HTML 文件并转换为 DOM 树
当我们打开一个网页时,浏览器都会去请求对应的 HTML 文件。虽然平时我 们写代码时都会分为 JS 、 CSS 、 HTML 文件,也就是字符串,但是计算机 硬件是不理解这些字符串的,所以在网络中传输的内容其实都是 0 和 1 这 些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为 字符串,也就是我们写的代码。
当数据转换为字符串以后,浏览器会先将这些字符串通过词法分析转换为标记 ( token ),这一过程在词法分析中叫做标记化( tokenization )
那么什么是标记呢?这其实属于编译原理这一块的内容了。简单来说,标记还 是字符串,是构成代码的最小单位。这一过程会将代码分拆成一块块,并给这 些内容打上标记,便于理解这些最小单位的代码是什么意思
当结束标记化后,这些标记会紧接着转换为 Node ,最后这些 Node 会根据 不同 Node 之前的联系构建为一颗 DOM 树
以上就是浏览器从网络中接收到 HTML 文件然后一系列的转换过程,在解析 HTML 文件的时候,浏览器还会遇到 CSS 和 JS 文件,这时 候浏览器也会去下载并解析这些文件
40、你在写项目的时候遇到过什么样的浏览器的兼容问题?你是怎么解决的?
浏览器兼容问题一:不同浏览器的标签默认的外补丁和内补丁不同
问题症状:随便写几个标签,不加样式控制的情况下,各自的margin 和padding差异较大。
碰到频率:100%
解决方案:CSS里 *{margin:0;padding:0;}
备注:这个是最常见的也是最易解决的一个浏览器兼容性问题,几乎所有的CSS文件开头都会用通配符*来设置各个标签的内外补丁是0。
浏览器兼容问题二:块属性标签float后,又有横行的margin情况下,在IE6显示margin比设置的大
问题症状:常见症状是IE6中后面的一块被顶到下一行
碰到频率:90%(稍微复杂点的页面都会碰到,float布局最常见的浏览器兼容问题)
解决方案:在float的标签样式控制中加入 display:inline;将其转化为行内属性
备注:我们最常用的就是div+CSS布局了,而div就是一个典型的块属性标签,横向布局的时候我们通常都是用div float实现的,横向的间距设置如果用margin实现,这就是一个必然会碰到的兼容性问题。
浏览器兼容问题三:设置较小高度标签(一般小于10px),在IE6,IE7,遨游中高度超出自己设置高度
问题症状:IE6、7和遨游里这个标签的高度不受控制,超出自己设置的高度
碰到频率:60%
解决方案:给超出高度的标签设置overflow:hidden;或者设置行高line-height 小于你设置的高度。
备注:这种情况一般出现在我们设置小圆角背景的标签里。出现这个问题的原因是IE8之前的浏览器都会给标签一个最小默认的行高的高度。即使你的标签是空的,这个标签的高度还是会达到默认的行高。
浏览器兼容问题四:行内属性标签,设置display:block后采用float布局,又有横行的margin的情况,IE6间距bug
问题症状:IE6里的间距比超过设置的间距
碰到几率:20%
解决方案:在display:block;后面加入display:inline;display:table;
备注:行内属性标签,为了设置宽高,我们需要设置display:block;(除了input标签比较特殊)。在用float布局并有横向的margin后,在IE6下,他就具有了块属性float后的横向margin的bug。不过因为它本身就是行内属性标签,所以我们再加上display:inline的话,它的高宽就不可设了。这时候我们还需要在display:inline后面加入display:talbe。
浏览器兼容问题五:图片默认有间距
问题症状:几个img标签放在一起的时候,有些浏览器会有默认的间距,加了问题一中提到的通配符也不起作用。
碰到几率:20%
解决方案:使用float属性为img布局
备注:因为img标签是行内属性标签,所以只要不超出容器宽度,img标签都会排在一行里,但是部分浏览器的img标签之间会有个间距。去掉这个间距使用float是正道。(我的一个学生使用负margin,虽然能解决,但负margin本身就是容易引起浏览器兼容问题的用法,所以我禁止他们使用)
浏览器兼容问题六:标签最低高度设置min-height不兼容
问题症状:因为min-height本身就是一个不兼容的CSS属性,所以设置min-height时不能很好的被各个浏览器兼容
碰到几率:5%
解决方案:如果我们要设置一个标签的最小高度200px,需要进行的设置为:
{
min-height:200px;
height:auto !important;
height:200px;
overflow:visible;
}
备注:在B/S系统前端开时,有很多情况下我们又这种需求。当内容小于一个值(如300px)时。容器的高度为300px;当内容高度大于这个值时,容器高度被撑高,而不是出现滚动条。这时候我们就会面临这个兼容性问题。
浏览器兼容问题七:透明度的兼容CSS设置
做兼容页面的方法是:每写一小段代码(布局中的一行或者一块)我们都要在不同的浏览器中看是否兼容,当然熟练到一定的程度就没这么麻烦了。建议经常会碰到兼容性问题的新手使用。很多兼容性问题都是因为浏览器对标签的默认属性解析不同造成的,只要我们稍加设置都能轻松地解决这些兼容问题。如果我们熟悉标签的默认属性的话,就能很好的理解为什么会出现兼容问题以及怎么去解决这些兼容问题。
/* CSS hack*/
我很少使用hacker的,可能是个人习惯吧,我不喜欢写的代码IE不兼容,然后用hack来解决。不过hacker还是非常好用的。使用hacker我可以把浏览器分为3类:IE6 ;IE7和遨游;其他(IE8 chrome ff safari opera等)
◆IE6认识的hacker 是下划线_ 和星号 *
◆IE7 遨游认识的hacker是星号 *
比如这样一个CSS设置:
height:300px;
*height:200px;
_height:100px;
IE6浏览器在读到height:300px的时候会认为高时300px;继续往下读,他也认识heihgt, 所以当IE6读到height:200px的时候会覆盖掉前一条的相冲突设置,认为高度是200px。继续往下读,IE6还认识_height,所以他又会覆盖掉200px高的设置,把高度设置为100px;
IE7和遨游也是一样的从高度300px的设置往下读。当它们读到*height200px的时候就停下了,因为它们不认识_height。所以它们会把高度解析为200px,剩下的浏览器只认识第一个height:300px;所以他们会把高度解析为300px。因为优先级相同且想冲突的属性设置后一个会覆盖掉前一个,所以书写的次序是很重要的。
41、vue中的router有什么区别?
1.$router是路由实例,而$route为当前router跳转对象。
2.$route包括path、params、hash、query、fullPath、matched、name等路由信息参数,而 $router包括了路由的跳转方法,钩子函数等,在script标签中想要导航到不同的URL,可使用$router.push方法。
42如何实现vue动态路由?
我们经常需要吧某种模式匹配到的所有路由,全都映射到同个组件。例如就是在一个User的组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:
const User = {
template: '<div>User</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
})
一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。于是,我们可以更新 User 的模板,输出当前用户的 ID:
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
当整个vue-router 注入到根实例后,在组件的内部,可以通过 this.$route 来获取到 router 实例。params 属性用来获得这个动态部分。它是一个对象,属性名,就是路径中定义的动态部分 id, 属性值就是router-link中to 属性中的动态部分。使用vuex时,组件中想要获取到state 中的状态,是用computed 属性,在这里也是一样,在组件中,定义一个computed 属性dynamicSegment, user 组件修改如下:
<template>
<div>
<h1>User</h1>
<div>我是user组件, 动态部分是{{dynamicSegment}}</div>
</div>
</template>
<script>
export default {
computed: {
dynamicSegment () {
return this.$route.params.id
}
}
}
</script>
还有一个问题就是我们在来回切换动态路由的时候,这时如果想要在组件来回切换的时候做点事情,由于是同一个组件,vue不会再销毁重新创建,而是复用,实现组件的生命周期不管用了,我们需要在组件内部(user.vue中)利用 watch 来监听$route 的变化。把上面的代码用监听$route 。
<script>
export default {
data () {
return {
dynamicSegment: ''
}
},
watch: {
$route (to,from){
// to表示的是你要去的那个组件,from 表示的是你从哪个组件过来的,它们是两个对象,你可以把它打印出来,它们也有一个param 属性
console.log(to);
console.log(from);
this.dynamicSegment = to.params.id
}
}
}
</script>
43、vue和react的区别。
1.监听数据变化
实现原理不同Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。为什么React不精确监听数据变化呢?这是因为Vue和React设计理念上的区别,Vue使用的是可变数据,而React更强调数据的不可变,两者没有好坏之分,Vue更加简单,而React构建大型应用的时候更加鲁棒。
2.数据流的不同
Vue1.0中可以实现两种双向绑定:父子组件之间,props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。Vue2.x中去掉了第一种,也就是父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改),并且Vue2.x已经不鼓励组件对自己的 props进行任何修改了。
React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。不过由于我们一般都会用Vuex以及Redux等单向数据流的状态管理框架,因此很多时候我们感受不到这一点的区别了。
3.HoC和mixins
Vue组合不同功能的方式是通过mixin,Vue中组件是一个被包装的函数,并不简单的就是我们定义组件的时候传入的对象或者函数。比如我们定义的模板怎么被编译的?比如声明的props怎么接收到的?这些都是vue创建组件实例的时候隐式干的事。由于vue默默帮我们做了这么多事,所以我们自己如果直接把组件的声明包装一下,返回一个HoC,那么这个被包装的组件就无法正常工作了。
React组合不同功能的方式是通过HoC(高阶组件)。React最早也是使用mixins的,不过后来他们觉得这种方式对组件侵入太强会导致很多问题,就弃用了mixinx转而使用HoC。高阶组件本质就是高阶函数,React的组件是一个纯粹的函数,所以高阶函数对React来说非常简单。
4.组件通信的区别
Vue中有三种方式可以实现组件通信:父组件通过props向子组件传递数据或者回调,虽然可以传递回调,但是我们一般只传数据;子组件通过事件向父组件发送消息;通过V2.2.0中新增的provide/inject来实现父组件向子组件注入数据,可以跨越多个层级。
React中也有对应的三种方式:父组件通过props可以向子组件传递数据或者回调;可以通过 context 进行跨层级的通信,这其实和 provide/inject 起到的作用差不多。React 本身并不支持自定义事件,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数,但Vue更倾向于使用事件。在React中我们都是使用回调函数的,这可能是他们二者最大的区别。
5.模板渲染方式的不同
在表层上,模板的语法不同,React是通过JSX渲染模板。而Vue是通过一种拓展的HTML语法进行渲染,但其实这只是表面现象,毕竟React并不必须依赖JSX。
在深层上,模板的原理不同,这才是他们的本质区别:React是在组件JS代码中,通过原生JS实现模板中的常见语法,比如插值,条件,循环等,都是通过JS语法实现的,更加纯粹更加原生。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如条件语句就需要 v-if 来实现对这一点,这样的做法显得有些独特,会把HTML弄得很乱。
举个例子,说明React的好处:react中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以我们import 一个组件完了之后,还需要在 components 中再声明下,这样显然是很奇怪但又不得不这样的做法。
6.渲染过程不同
Vue可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
React在应用的状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化。
如果应用中交互复杂,需要处理大量的UI变化,那么使用Virtual DOM是一个好主意。如果更新元素并不频繁,那么Virtual DOM并不一定适用,性能很可能还不如直接操控DOM。
7.框架本质不同
Vue本质是MVVM框架,由MVC发展而来;
React是前端组件化框架,由后端组件化发展而来。
8.Vuex和Redux的区别
从表面上来说,store注入和使用方式有一些区别。在Vuex中,store来读取数据。在Redux中,我们每一个组件都需要显示的用connect把需要的props和dispatch连接起来。另外,Vuex更加灵活一些,组件中既可以dispatch action,也可以commit updates,而Redux中只能进行dispatch,不能直接调用reducer进行修改。
从实现原理上来说,最大的区别是两点:Redux使用的是不可变数据,而Vuex的数据是可变的,因此,Redux每次都是用新state替换旧state,而Vuex是直接修改。Redux在检测数据变化的时候,是通过diff的方式比较差异的,而Vuex其实和Vue的原理一样,是通过getter/setter来比较的,这两点的区别,也是因为React和Vue的设计理念不同。React更偏向于构建稳定大型的应用,非常的科班化。相比之下,Vue更偏向于简单迅速的解决问题,更灵活,不那么严格遵循条条框框。因此也会给人一种大型项目用React,小型项目用Vue的感觉。
43、vue双向绑定是如何实现的?
Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter / setter,当数据变化时通知视图更新
所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。如下图:
- 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
- data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。
let car = {
'brand':'BMW',
'price':3000
}
通过Object.defineProperty()方法给car定义了一个price属性,并把这个属性的读和写分别使用get()和set()进行拦截,每当该属性进行读或写操作的时候就会出发get()和set()。如下图:
实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。
基于数据劫持的双向绑定离不开Proxy与Object.defineProperty等方法对对象/对象属性的"劫持",我们要实现一个完整的双向绑定需要以下几个要点。
利用Proxy或Object.defineProperty生成的Observer针对对象/对象的属性进行"劫持",在属性发生变化后通知订阅者
解析器Compile解析模板中的Directive(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染
Watcher属于Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化
44、vueRouter的使用和原理是怎样的?
单页面应用与多页面应用
单页面
即 第一次进入页面的时候会请求一个html文件,刷新清除一下。切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了。
原理是:JS会感知到url的变化,通过这一点,可以用js动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前页面上,这个时候的路由不是后端来做了,而是前端来做,判断页面到底是显示哪个组件,清除不需要的,显示需要的组件。这种过程就是单页应用,每次跳转的时候不需要再请求html文件了。
多页面
即 每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。
原理是:传统的页面应用,是用一些超链接来实现页面切换和跳转的
其实刚才单页面应用跳转原理即 vue-router实现原理
vue-router实现原理
原理核心就是 更新视图但不重新请求页面。
vue-router实现单页面路由跳转,提供了三种方式:hash模式、history模式、abstract模式,根据mode参数来决定采用哪一种方式。
路由模式
vue-router 提供了三种运行模式:
● hash: 使用 URL hash 值来作路由。默认模式。
● history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
● abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端
Hash模式
hash即浏览器url中#后面的内容,包含#。hash是URL中的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。
也就是说
● 即#是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中,不包含#。
● 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。
所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。
History模式
HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;
由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
有时,history模式下也会出问题:
eg:
hash模式下:xxx.com/#/id=5 请求地址为 xxx.com,没有问题。
history模式下:xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误;
为了应对这种情况,需要后台配置支持:
在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
abstract模式
abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)。