JavaScript
不同数据类型间的比较

答案:true true false
- 解析:
第一个输出:将[]转为布尔类型,为true。只有以下类型转为布尔值为false:Boolean(null)、Boolean(undefined)、Boolean(0)、Boolean(‘’)、Boolean(NaN)
第二和第三:其他类型与布尔类型比较,将两个类型转为数字再进行比较。Number([])=0,Number(false)=0,所以第二个输出true。Number({})=NaN,NaN==0 --> false,所以第三个输出false
Number([2]) //2
Number(['x']) //NaN
Number(undefined); //NaN
Number(null); //0
null+1=1
undefined+1=NaN
undefined==false; //false
false=='' //true
null==undefined; //true,都转为boolean类型
null=undefined; //false,不强制转换
FormData
- 创建FormData对象
- 像创建对象一样创建,无参数
- 将已有的表单对象作为参数来创建FormData对象
HTML部分
<form action="" id='form'>
<label for="">
姓名: <input type="text" name="name">
</label>
<label for="">
文件:<input id="file" type="file" name="file">
</label>
<label for="">
<input type="button" value="保存">
</label>
</form>
JS部分
var form=document.getElementById('form');
var formdata=new FormData(form);
var name=formdata.get(name);
var file=formdata.get(file);
formdata.append('token','sdcsdc');
//提交数据
var xhr=new XMLHttpRequest();
xhr.open("post","http://127.0.0.1/adv");
xhr.send(formdata);
xhr.onload=function(){
if(xhr.readyState==4&&xhr.status==200){
//...
}
}
- 往FormData对象添加数据 -- 数据类型是键值对
- FormData.append( key,value [,filename] )
- filename:当添加的数据是file对象就可以添加这个参数,用来设置传递给服务器的文件名称(可选参数)
var formdata=new FormData();
formdata.append('name','jack');
formdata.append('name','rose');
formdata.get('name'); //jack
formdata.getAll('name'); //[jack,rose]
- 获取数据
- FormData.get(key)
- FormData.getAll(key)
- 判断是否存在某个值
- FormData.has(key)
- 遍历数据
- FormData.keys():返回所有数据的键值
- FormData.values():返回所有数据的值
- FormData.entries():返回FormData对象的迭代器
formData.append("k1", "v1");
formData.append("k1", "v2");
formData.append("k2", "v1");
var i = formData.entries();
i.next(); // {done:false, value:["k1", "v1"]}
i.next(); // {done:fase, value:["k1", "v2"]}
i.next(); // {done:fase, value:["k2", "v1"]}
i.next(); // {done:true, value:undefined}
//返回的对象的value属性以数组的形式,第一个是key,第二个为value
//以循环的形式
for(var pair of formData.entries()){ //pair是每次迭代获取到的对象里的value
console.log(pair[0]+':'+pair[1])
//k1:v1
//k1:v2
//k2:v1
}
- 修改数据
- FormData.set(key,value) :若key不存在会自动创建
- 删除数据
- FormData.delete(key):即使key不存在也不会出错
js垃圾回收机制
- 标记清除:给存储在内存中的变量都添加标记,然后去掉那些处在环境中的变量和被环境中的变量引用的变量的标记,剩下被标记的就是将要删除的变量。然后垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收他们占用的空间。
- 引用计数:语言引擎有一张引用表,保存内存里所有资源的引用次数。如果一个值的引用次数是0,则表示这个值不再用到了,因此可以将这块内存释放。
线程与进程的区别
- 进程是CPU资源分配的最小单位
- 线程是程序执行时的最小单位,是CPU调度和分派的最小单位
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 一个进程可以由多个线程组成,线程间共享进程的所有资源,每个线程有自己的局部变量和堆栈,而进程有自己的独立地址空间。
- 线程由CPU独立调度执行,多CPU环境下就允许多个线程同时执行。
- 单线程(单个CPU运行):比如,打开任意一个浏览器,里面有多个标签页;当某一个标签页系统崩溃时,其他标签页也不能使用,必须关掉浏览器。
- 多线程(多个cpu等运行):比如,浏览器中有多个标签页,当某一个标签页系统崩溃时,只是该标签页不能使用,不影响其他标签页的正常使用。

答案:
- 每个线程对a均做了两次操作:+1,-2
- 当线程1与线程2不并发时:1执行完后a=-1,2使用-1作为a的初值,执行完后a=-2
- 当线程1与线程2并发时:此时读写冲突,相当于只有一个线程对a的读写最终生效,结果a=-1
- 当线程1与线程2部分并发:1执行到第一个操作时,a=1。此时线程2开始。1的读写被2覆盖,2把a=1作为初值,结果为0
逗号运算符,=号运行符,作用域
下面输出结果是:
var out = 25,
inner = {
out: 20,
func: function () {
var out = 30;
return this.out;
}
};
console.log((inner.func, inner.func)());
console.log(inner.func());
console.log((inner.func)());
console.log((inner.func = inner.func)());
结果:25 20 20 25
- 第一个输出:逗号运算符是运算前面的,返回最后一个的结果,此时返回的是最后一个inner.func的结果,即是一个匿名函数。
例子
let a=1,b=2,k=0;
console.log( (a++,b++,k++) ); //输出0,注意是要把整个括起来,然后将计算后的结果返回
console.log(a); //2
console.log(b); //3
console.log(k); //1
- 第二个输出和第三个输出:都是对象调用函数,函数的上下文this是对象
- 第四个输出:等号运算符返回的是赋值后的结果,即返回一个匿名函数
例子:
let a=3,b;
console.log(b=a); //输出3
引用类型的赋值,等号运算符与.运算符的优先级问题
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x);
console.log(b.x);
答案:
undefined
{n:2}
分析:
- b和a指向同一地址,对象{n:1}的地址
- 在a.x = a = {n: 2};中,由于.号运算符优先级更高,所以先进行a.x
- 那究竟是a.x=a,a={n:2}还是a = {n: 2} && a.x = {n:2} 还是 a.x = {n:2} && a= {n:2}。借助Proxy来说明:
const obj = new Proxy({}, {
set(target, key, value, r) {
console.log(key, value)
if (key === 'a') Reflect.set(target, key, 'isA', r);
else Reflect.set(target, key, value, r);
}
});
obj.b = obj.a= {n: 1};
// 输出:
// "a" {n: 1}
// "b" {n: 1}
obj.a; // isA
obj.b; // {n: 1}
可以得出 赋值的顺序是从右边开始到左边的。
而且是直接 a = {n: 1}, a.x = {n:1 },而不是 a.x = a 这样去赋值
再借助 Proxy 来分析一开始part1这道题,用obj.a, obj.b 来代替原题目的 a和b。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.a = {n: 1 };// setting a;
obj.b = obj.a; // getting a; setting b;
obj.a.x = obj.a = {n:2 }; // getting a; setting a;
*****************分割线**********************************
console.log(obj.a); //{n;2}
obj.a.n=3; //getting a
console.log(obj.b); //getting b {n:1,x:{n:3}}
第一部分:
可以看到obj.a.x = obj.a = {n: 2}这段语句执行时,会先输出一个 getting a 再输出 setting a。
这就意味着在对 obj.a.x 赋值时,程序是先获取 obj.a指向的对象的内存地址,此时触发了 getting a,
然后再对右边 obj.a 进行赋值,触发了 setting a, 赋值完最后一步才是对 obj.a.x赋值 {n:2 }。
注意:最后一步对obj.a.x赋值时,不会再去读取obj的a,则说明此时的a仍是指向原来的地址-- 对象{n:1}的地址,也是obj.b指向的地址。
赋值之后,此时obj.a(和obj.b不再指向同一地址)和obj.b.x指向的是同一地址 -- {n:2}的地址
第二部分:
在分割线之下,obj.a.n=3; 读取的a再赋值,改变了指向的对象{n:2}的值
而obj.b.x也指向该对象
变量提升,(字符串/变量 in obj)的含义
if (!("a" in window)) {
var a = 1;
}
alert(a); //undefined
- "a" in window:意思是window对象是否有a这个属性,写成a in window也可以
此时代码的意思是,如果没有这个属性,则创建这个属性且赋值。
但if不是代码块,存在变量提升,所以上面代码实际为
var a;
if (!("a" in window)) {
a = 1;
}
alert(a);
作用域
函数内属于一个作用域,try和catch不属于一个作用域
(function f() {
try{
throw new Error()
}catch (x) {
var x=1,y=2;
var t=3;
console.log(x);
}
console.log(t); //3
console.log(x); //undefined
console.log(y); //2
})();
在f函数中,x、y、t会变量提升
在catch中,var x=1,就近原则,此时对x的赋值其实是对形参的赋值,不是f函数里的x,所以函数f的x在catch外访问依旧是undefined
函数声明、函数表达式
声明变量b是一个函数,使用函数表达式
1、let b=function(){}
2、let b=function f(){} //可以为函数起一个名字,但是'f'不属于window的属性,则无法调用f(),相当于匿名
下面程序的输出结果?
var a = 1;
var b = function a(x) {
x && a(--x);
};
alert(a); //1
console.log(b(a)) //undefined 因为函数没有return,有return的话就是0
/*
在函数外部,a是number类型,在外面调用函数a会出错,但在函数内部调用不会出错
上面其实等同于 var b=function(x){...}
*/
- 函数声明和变量声明都存在提升,但函数表达式不会,只会提升引用的变量。
- 如果函数名和变量重名,函数声明会覆盖变量声明,但赋值不会,谁后就是那个值。即函数声明级别比变量声明高。
例子:
function a(){console.log('function')}
var a;
console.log(typeof a) //function
//如果变量赋值了
function a(){console.log('function')}
var a=1;
console.log(typeof a) //number
函数内的变量提升,与全局变量重名
var a = 4;
function b() {
a = 3;
console.log(a);
function a(){};
}
b(); //3
console.log(a); //4
函数b内的函数a会提升,然后被a=3覆盖了值,所以输出3
挖坑:
var a = 4;
function b() {
a = 3;
}
console.log(a); //4
为什么不是输出3?—— 因为没有调用b()....
数据类型
- 基本数据类型:string , number , boolean , undefined , null , symbol(ES6)
- 引用类型:对象、数组、函数
typeof返回的数据类型
string、number、boolean、undefined、object、function、symbol
null与undefined
- null指的是一个对象为空,即变量是有值的,值为null
- undefined:表示的是一个变量/属性没有设置值。
- null==undefinde —— true, null===undefinde —— false
JSON.stringify转化null与undefined
JSON.stringify({1:undefined}) //“{}”
JSON.stringify({0:null}) //“{“0”:null}”
返回undefined的情况
- 当一个变量未赋值
- 访问对象的某个属性不存在
- 函数没有返回值
- 函数应该传递的参数没有提供,该参数等于undefined
symbol
- symbol值是通过Symbol函数生成,没有new,生成的Symbol是一个类似于字符串的原始类型的值。
- Symbol(para):para可以是字符串/对象,对象会调用toString方法转为字符串
- 传入的参数只是对当前Symbol值的描述,不是返回的值,所以即使传入相同的参数,Symbol的返回值也是不同的
var sm=Symbol('xx');
console.log(sm); //Symbol('xx')
typeof sm; //'symbol'
var ss1=Symbol('xx');
console.log(ss==ss1); //false
ss=ss1; //因为是基本数据类型,所以可以用=令两个值相等
console.log(ss===ss1); //true
js常见的内置对象
object、String、Number、Boolean、Math、Array、Function、JSON
如何理解JSON
- JSON是JS一个内置对象,含有两个函数,stringify和parse
- 同时JSON也是一种轻量级的数据交换格式,以{}括起来
- JSON 与 JS 对象的关系:JSON是JS对象的字符串表示法,它使用文本表示一个 JS 对象的信息,本质是一个字符串。
- 任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。
JS的内置函数
String、Number、Boolean、Object、Function、RegExp、Error、Date、Array
日期Date
Date.now():返回当前时间毫秒数
var dt=new Date();
dt.getFullYear(); //年
dt.getMonth(); //月(0-11)
dt.getDate(); //日(1-31),返回一个月中的某一天
dt.getDay(); //天(0-6),返回一星期中的第几天(0表示星期日)
dt.getHours() //小时(0-23)
dt.getMinutes() //分钟(0-59)
dt.getSeconds() //秒数(0-59)
dt.getTime() //返回从1970/1/1 至今的毫秒数
##有对应的set方法
对象的键
var a={},
b={key:'b'},
c={key:'c'};
a[b]=123;
a[c]=456;
console.log(a[b]); //456
解析:
因为键的名称只能是字符串,b/c作为键会调用toString得到的都是[object Object],所以a[b]和a[c]都是a["[object Object]"]
this
- 函数直接加()调用,this=window
- 对象打点调用实例函数,this是这个对象
- 从数组/类数组中枚举出函数(即函数是数组的元素)调用,this是这个数组
function fun1(fn){ //第二步
arguments[0](3,4); //类数组枚举调用传入的fun2(3,4)
}
function fun2(){
alert(this.length); //此时this是fun1的arguments,表示fun1传入的实参个数--5
alert(arguments.length); //此时表示fun2传入的实参个数-- 2
}
fun1(fun2,5,6,7,8); //第一步
- 定时器的回调函数,this=window
- 事件处理函数,this是绑定事件的元素
- 用new调用函数,this是创建的实例对象
- call、apply绑定this,如果没有传参或传的是null,则this指向window
setTimeOut
for(var i=0;i<5;i++){
setTimeout(console.log(i),0) //第一个参数不是函数
//0
//1
//2
//3
//4
}
数组的方法
- 对于那些遍历数组的每一项,然后每一项都去执行回调的方法,都要有return,除了forEach
- lastIndexOf(val [,startIndex]):如果未传入第二个参数,则从尾向头查找第一次出现val的位置,如果指定了第二个参数,则从这个参数向前找。用这个方法可以找某个元素 在该数组最后一次出现的位置,因为是从后往前找。
parseInt
parseInt(string [,radix]):解析一个字符串参数,并返回一个指定基数的整数
参数:
- string:要被解析的值,如果参数不是一个字符串,则将其转换为字符串,使用toString,字符串开头的空白符会被忽略。
- radix:一个介于2-36之间的整数,表示把第一个参数看作是一个数的几进制表示,默认为10
返回值:
- 返回的是第一个参数的十进制表示
如果radix为undefined或0或未指定,
1、如果string以‘0x’或‘0X’开头,则基数是16(16进制)
2、如果string以‘0’开头,则基数是8或10进制,具体是哪个进制由实现环境决定
3、如果字符串string以其他任何值开头,则基数是10
例子:
['10','10','10','10','10'].map(parseInt);
// [10, NaN, 2, 3, 4]
实际执行的代码是:
['10','10','10','10','10'].map((item, index) => {
return parseInt(item, index)
})
//第一个radix为0,属于上面所说的情况,此时看item,不是以0或0x开头,则基数是10,10的十进制就是10
//第二个 10,radix是1,表示10是1进制表示的数字,错误
....
异步过程
1)主线程发起一个异步请求
2)相应的工作线程接收到请求后告知主线程已收到(异步函数返回)
3)主线程可以继续执行下面的代码,同时工作线程执行异步任务。
4)等到工作线程完成任务后,通知主线程
5)主线程收到通知后,执行一定动作(调用回调函数)。
JS引擎中负责解释和运行js代码的只有一个,叫做主线程,还有其他线程,例如处理Ajax请求的线程,处理DOM事件的线程,定时器线程等工作线程(可能存在与js引擎内或外)。
综上,可以总结为两个要素:
1、发起函数(注册函数)-- 用来发起异步过程
2、执行回调函数 -- 用来处理结果
两个函数都是在主线程上调用的
异步任务执行完毕后需要通知主线程,这个通知机制是如何实现的?—— 消息队列和事件循环
异步任务执行完毕后,工作线程将回调函数封装成消息message放到消息队列中,主线程通过事件循环过程去取出消息
- 消息队列:是一个先进先出的队列,存放各种消息
- 事件循环:指的是主线程重复的从消息队列里取出消息并执行的过程,取出一个消息并执行的过程就叫做一次循环。
- 同步和异步任务分别进入不同的执行场所,同步进入主线程,异步进入Event table并注册回调函数
- 异步任务执行完毕后,会将注册的函数加入事件队列里
- 当主线程内的任务执行完毕后,会去事件队列里读取消息,到主线程执行回调函数
- 重复以上过程就是事件循环
例子:
setTimeout(fn,2000)
setTimeout就是发起异步过程的函数,fn就是回调函数,但是回调函数并不一定要作为发起函数的参数,如:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url); //发起异步请求的函数
xhr.send(); // 发起函数
宏任务和微任务
除了广义的同步任务和异步任务之外,还有宏任务和微任务
- 宏任务:整体的js代码、setTimeout、setInterval、setImmediate
- 微任务:promise.then,process.nextTick(优先级比promise.then高,比它先执行)
不同的任务会进入不同的事件/任务队列,比如setTimeout和setInterval会进入相同的Event Queue。
先执行宏任务队列的一个,然后再执行宏任务下的所有微任务,再去渲染,完成一次事件循环。然后再执行下一个宏任务。
事件循环的一次循环,就是执行事件队列里的一个任务。先执行宏任务,再执行宏任务里的微任务,这样就是一次循环,第一轮循环执行后,在执行下一次循环
例子
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
结果:1,7,6,8,2,4,3,5,9,11,10,12。
第一轮事件循环:
- 整个js代码作为第一个宏任务进入事件队列里,遇到console.log,输出1。
- 遇到setTimeout,属于异步任务,等到异步任务完成后,回调函数被分发到宏任务的事件队列里,我们暂时标记为setTimeout1
- process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
- 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
- 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。此时宏任务队列里就有两个回调函数了。
- 第一轮循环的宏任务执行完后,有两个微任务,process1和then1
- 执行process1,输出6。
- 执行then1,输出8。
第二轮事件循环:
- 执行第二个宏任务,从setTimeout1宏任务开始,输出2
- 接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。
- new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
- 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行
- 输出3
- 输出5
第三轮事件循环:
- 只剩setTimeout2了,执行。输出9。
- 将process.nextTick()分发到微任务Event Queue中。记为process3。
- 直接执行new Promise,输出11。 将then分发到微任务Event Queue中,记为then3。
- 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3
- 输出10
- 输出12
例子
process.nextTick(() => { //微任务
console.log('nextTick')
})
Promise.resolve()
.then(() => { //微任务
console.log('then')
})
setImmediate(() => { //宏任务
console.log('setImmediate')
})
console.log('end')
运行结果:
end
nextTick
then
setImmediate
微任务里的微任务
process.nextTick(()=>{
console.log(1)
process.nextTick(()=>{
console.log(2)
})
})
process.nextTick(()=>{
console.log(3)
})
console.log('run')
run
1
3
2
promise
promise是异步编程的解决方案,比传统的异步编程解决方案【事件】和【回调函数】更加合理和强大。
promise对象用于异步操作,它表示未完成且预计未来完成的异步操作
- promise对象可以通过Promise构造函数创建,Promise构造函数需要传递一个函数作为参数,当创建Promise实例时,则会调用该函数。
- 这个函数参数有两个参数,resolve和reject函数,用来改变promise对象的状态。
- promise有三个状态,分别是pending、fulfilled和rejected。只能从pending转为fulfilled或rejected,状态确定之后不可改变
- promise原型上有then函数,传递的参数是fulfilled状态和rejected状态执行的回调,回调的参数就是promise的值,则resolve和reject函数的实参
- then函数返回一个新的promise实例,该实例的状态与回调函数的执行情况和返回值有关。默认返回一个resolved状态。如果抛出错误,则返回的promise实例就是rejected状态。如果返回一个新的promise实例,则取决于这个promise的状态,等待它状态改变才会执行下面的then的回调
- promise原型上还有catch方法,可以捕获then回调抛出的错误
- 原型上还有race方法,可以传入多个promise任务,多个任务同步执行,最终返回第一个执行结束的promise任务的结果
- 原型上还有all方法,只有所有的promise任务都执行成功才会调用成功的回调,只要有一个失败则调用失败的回调
- 原型上还有resolve,用来创建promise实例。
- Promise.resolve()可以传一个普通值,返回一个resolved类型的promise实例
- 可以传入一个promise实例,返回的就是这个promise实例
- 可以传入thenable对象,返回的promise实例的状态取决于这个对象的最终状态
- 原型上还有reject方法,也是用来创建promise对象,无论传入什么值,返回的都是一个rejected类型的promise实例
在没有 promise 之前,怎么解决异步回调
- 回调函数
- 事件监听
- 订阅发布模式 资料
async与await
使异步变同步
- async定义的函数表示该函数是一个异步函数,调用该函数时,不会阻塞其他代码的运行
- 当async会封装一个由Promise.resolve()创建的Promise实例,然后返回。如果抛出错误,则封装一个Promise.reject()返回。如果显式写了return一个值,那这个值就是返回的promise实例的值。
- 如果要获取Promise的值,需要写then函数,传入回调
- async函数不一定需要await,但await必须包含在async内
- await表示等待的意思,等待后面的代码返回结果之后再继续指向。
- await后面可以是任何表达式,但通常是一个返回Promise实例的表达式
async function timeout() {
return 'hello world'
}
console.log(timeout());
console.log('虽然在后面,但是我先执行');
结果
Promise {<resolved>: "hello world"}
test.html:85 虽然在后面,但是我先执行
获取Promise的值
async function timeout() {
return 'hello world'
}
timeout().then(result => {
console.log(result);
})
console.log('虽然在后面,但是我先执行');
结果
虽然在后面,但是我先执行
hello world
await
function doubleAfter2seconds(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2 * num)
}, 2000);
} )
}
async function testResult() {
let result = await doubleAfter2seconds(30);
console.log(result); //60
}
testResult();
Ajax
var xhr=new XMLHttpRequest();
xhr.open('get','/index.js',true) //true表示异步
xhr.onreadystatechange=function () {
if(xhr.readyState===4)
if(xhr.status===200){
console.log(xhr.responseText)
}
}
xhr.send(null); //将请求发送给服务端,仅用于post请求
xhr.open()的第三个参数表示是否异步
XMLHttpRequest 对象如果要用于 AJAX 的话,其 open() 方法的 async 参数必须设置为 true
xhr.send():当使用初始化)send方法还未调用
1:(载入)已调用send方法,正在发送请求
2:(载入完成)send方法已经执行完毕,已经接收到所有响应内容
3:(交互)正在解析响应内容
4:(完成)响应内容解析完成,可以在客户端调用
如何保证cookie的安全性
- 对保存到cookie的私密数据进行加密
- 设置http-only=true
- 给cookie设置有效期
- 给cookie加个时间戳和ip戳,实际就是让cookie在同个ip下过多少时间后失效
cookie、localStorage、sessionStorage的区别
cookie
- 因为HTTP是无状态的,对事务没有记忆能力。cookie是网站用来标识用户身份的(比如存储用户信息),存储在本地的数据。
- 在第一次请求服务端时,服务端在响应报文头添加set-cookie属性,属性值即是用户的cookie值。浏览器在下次请求时会自动在请求报文头加上Cookie属性,属性值即由服务端产生。
- cookie数据始终在同源的HTTP请求中携带(即使不需要),在浏览器和服务端之间来回传输
storage
- sessionStorage和localStorage都是在本地保存,不会发送给服务端
共同点:
- 都是保存在客户端,且同源的。
数据有效时间
- localStorage是永久性保存,除非手动删除
- sessionStorage保存的数据在当前窗口关闭后自动删除
- cookie数据在cookie设置的过期时间前一直有效,即使关闭窗口或浏览器
作用域不同:
- sessionStorage在不同的浏览器窗口不共享
- cookie和localStorage在所有同源窗口共享
cookie和session的区别
- cookie是保存在客户端,服务器能够知道其中的信息
- session是保存在服务端,客户端不知道其中的信息
- cookie保存的是字符串,session保存的是对象
- session不能设置路径,同一个用户在访问一个网站的期间,所有的session在任何一个地方都能访问到
- cookie能设置路径参数,那么同一个网站的不同路径下的cookie互相是访问不到的
- Session的实现是基于Cookie:Session技术是将数据存储在服务器端的技术,会为每个客户端都创建一块内存空间 存储客户的数据,但客户端需要每次都携带一个标识ID去服务器中寻找属于自己的内存空间。
从敲入 URL 到渲染完成的整个过程
- 用户输入url地址,浏览器根据DNS解析域名,得到IP地址
- 经过三次握手建立TCP连接
- 浏览器向服务器发送HTTP请求
- 服务端接收请求,将html文档返回给浏览器
- 浏览器接收文档后,如果有压缩则解压处理,然后就是页面解析
- 浏览器将文档下载下来后,便开始从上到下解析,解析完成之后,会生成DOM。如果页面中有css,会根据css的内容形成CSSOM,然后DOM和CSSOM会生成一个渲染树,最后浏览器会根据渲染树的内容计算出各个节点在页面中的确切大小和位置,并将其绘制在浏览器上。
- 若渲染过程遇到js文件,html文档会挂起渲染的线程,等待js文件加载和解析完毕,才可恢复html文档的渲染进程。
DOMContentLoaded和onload的区别
DOMContentLoaded:顾名思义,就是dom内容加载完毕。那什么是dom内容加载完毕呢?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成。
onload:页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件
对url进行编码的函数:escape,encodeURI,encodeURIComponent
- escape(url):该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。其他所有的字符都会被16进制的转义序列替换。 因此如果想对URL编码,最好不要使用此方法。
- encodeURI(url):该方法的目的是对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,#
- 该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。 其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。 所以可以用来将连接组件(协议、主机号、端口号)间的符号转换
var str='https://www.bilibili.com/text.html'
console.log(escape(str));
console.log(encodeURI(str));
console.log(encodeURIComponent(str));
encodeURI("http://www.w3school.com.cn/My first/")
输出
https%3A//www.bilibili.com/text.html
test.html:59 https://www.bilibili.com/text.html
test.html:60 https%3A%2F%2Fwww.bilibili.com%2Ftext.html
http://www.w3school.com.cn/My%20first/
性能优化:将js放到body标签底部
- 问题: js会阻塞文档的解析,页面的渲染,但生成完整的dom树是需要读完整个文档,那script无论放在哪都一样
- 实际: 浏览器为了更好的用户体验,浏览器能够渲染不完整的dom树和cssDom,尽快减少白屏的时间,这就是浏览器的first paint。
- 总结: 所以如果把script标签放在文档头部,js会阻塞后续文档的加载,阻塞解析dom,阻塞first paint,导致白屏时间延长,但是不会减少DOMContentLoaded被触发的时间。
document.write 和 innerHTML 的区别
- 如果想获取document,使用document.documentElement
- document.write是重写整个document内容
- innerHTML是HTMLElement的属性,可以精确到一个具体的元素,能读和写一个元素内部的html内容
script 标签的 defer 和 async 标签的作用与区别
- :没有defer和async属性,加载到该标签时,浏览器会立即加载和执行相应的脚本,会阻塞后面的文档的加载,
- :有了async属性之后,js脚本的加载与后续文档的加载和渲染是并行执行的,即异步执行。脚本加载之后立即执行,脚本执行时会阻塞 HTML 解析。
- :有了defer属性,后续文档的加载与js脚本的加载(不包括执行)是并行执行的,即异步操作。js的执行会等到文档所有元素解析完成之后,DOMContentLoaded事件触发执行之前。
因此,defer与async的区别就是js加载之后何时执行:
它们在加载脚本都是异步执行
async在加载之后就会立即执行
如果存在多个有defer属性的脚本,那么它们是按照加载顺序执行脚本的
都只适用于外部脚本文件,对与内联的 script 标签是不起作用
所以如果一定要将script放在头部,但又不想阻塞页面的加载,则加上defer属性
JS 识别不同浏览器信息
function myBrowser() {
var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
var isOpera = userAgent.indexOf("Opera") > -1;
if (isOpera) {
return "Opera"
}; //判断是否Opera浏览器
if (userAgent.indexOf("Firefox") > -1) {
return "Firefox";
} //判断是否Firefox浏览器
if (userAgent.indexOf("Chrome") > -1) {
return "Chrome";
} //判断是否Google浏览器
if (userAgent.indexOf("Safari") > -1) {
return "Safari";
} //判断是否Safari浏览器
if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera) {
return "IE";
}; //判断是否IE浏览器
}
HTML、XML与JSON
- HTML是超文本标记语言,除了可以标记文本之外,标记 图片,视频,链接 等其他内容。
- XML是可扩展性的标记语言,就是在文档里加上标签来说明里面的数据是什么意思。方便存储、传输、分享数据。与HTML的区别是,HTML标签是预定义的,XML是可扩展的。
- JSON是比较轻量级的数据交换格式,由键值对组成,方便读写,格式都是经过压缩的,占用带宽小。
编写一个方法,求一个字符串的字节长度
- String.prototype.charAt(index):返回的是字符
- String.prototype.charCodeAt(index):返回的是字符编码
假设:一个英文字符占用一个字节,一个中文字符占用两个字节
function getBytes(str){
var len = str.length; //获取字符串长度
var bytes = len;
for(var i = 0; i < len; i++){
if (str.charCodeAt(i) > 255) bytes++; //中文则+1
}
return bytes;
}
alert(getBytes("你好,as"));
new操作符具体干了什么?
- 创建一个新对象
- 这个新对象的原型(Object.getPrototypeOf(target))指向构造函数的prototype对象
- 该函数内的this会绑定在新创建的对象上
- 如果函数没有返回其他对象,那么会自动返回这个新对象
原型
function f(){}
typeof f.prototype; //"object"
typeof f.__proto__; //"function"
f.__proto__===Function.prototype //true
typeof Function.prototype; //"function"
- 所有引用类型(对象、数组、函数)都有__proto__隐式属性
- 由于兼容性问题,现在使用 Object.getPrototypeOf(target) 来替代 proto
- 使用 let obj=Object.create(target),让obj.proto=target
- Object.setPrototypeOf(target)(写操作)
- prototypeObj.isPrototypeOf(object):测试一个对象是否存在于另一个对象的原型链上
- obj.hasOwnProperty(prop): 指示对象自身属性中是否具有指定的属性,拒绝查找原型链
有哪些性能优化的方法?
web 前端是应用服务器处理之前的部分,前端主要包括:HTML、CSS、javascript、image 等各种资源,针对不同的资源有不同的优化方式。
内容优化
- 减少 HTTP 请求数。这条策略是最重要最有效的,因为一个完整的请求要经过 DNS 寻址,与服务器建立连接,发送数据,等待服务器响应,接收数据这样一个消耗时间成本和资源成本的复杂的过程。 常见方法:合并多个 CSS 文件和 js 文件,利用 CSS Sprites 整合图像,Inline Images (使用 data:URL scheme 在实际的页面嵌入图像数据 ),合理设置 HTTP 缓存等。
- 减少 DNS 查找
- 避免重定向
- 使用 Ajax 缓存
- 延迟加载组件,预加载组件
- 减少页面上的DOM 元素数量。页面中存在大量 DOM 元素,会导致 javascript 遍历 DOM 的效率变慢。
- 最小化 iframe 的数量。iframes 提供了一个简单的方式把一个网站的内容嵌入到另一个网站中。但其创建速度比其他包括 JavaScript 和 CSS 的 DOM 元素的创建慢了 1-2 个数量级。
- 避免 404。HTTP 请求时间消耗是很大的,因此使用 HTTP 请求来获得一个没有用处的响应(例如 404 没有找到页面)是完全没有必要的,它只会降低用户体验而不会有一点好处。
服务器优化
- 使用内容分发网络(CDN)。把网站内容分散到多个、处于不同地域位置的服务器上可以加快下载速度。
- GZIP 压缩
- 设置 ETag:ETags(Entity tags,实体标签)是 web 服务器和浏览器用于判断浏览器缓存中的内容和服务器中的原始内容是否匹配的一种机制。
- 提前刷新缓冲区
- 对 Ajax 请求使用 GET 方法
- 避免空的图像 src
- Cookie 优化,减小 Cookie 大小针对 Web组件使用域名无关的 Cookie
CSS 优化
- 将 CSS 代码放在 HTML 页面的顶部
- 避免使用 CSS 表达式
- 使用 < link> 来代替 @import
- 避免使用 Filters
javascript 优化
- 将 JavaScript 脚本放在页面的底部。
- 将 JavaScript 和 CSS 作为外部文件来引用。 在实际应用中使用外部文件可以提高页面速度,因为 JavaScript 和 CSS 文件都能在浏览器中产生缓存。
- 缩小 JavaScript 和 CSS
- 删除重复的脚本
- 最小化 DOM 的访问。使用 JavaScript 访问 DOM 元素比较慢。
- javascript 代码注意:谨慎使用 with,避免使用 eval Function 函数,减少作用域链查找。
图像优化
- 优化图片大小
- 通过 CSS Sprites 优化图片
- 不要在 HTML 中使用缩放图片
- favicon.ico 要小而且可缓存
类数组转为数组
- Array.prototype.slice.call(类数组 [,0])
- Array.from(类数组)
应用:将节点列表 (NodeList) 转换为数组
如果你运行 document.querySelectorAll("p") 方法,它可能会返回一个 DOM 元素的数组 — 节点列表对象。 但这个对象并不具有数组的全部方法,如 sort(),reduce(), map(),filter()。 为了使用数组的那些方法,你需要把它转换为数组。
var elements = document.querySelectorAll("p"); // NodeList
var arrayElements = [].slice.call(elements); // 现在 NodeList 是一个数组
var arrayElements = Array.from(elements); // 这是另一种转换 NodeList 到 Array 的方法
jq 的 ready 和 onload 事件的区别
- onload 是等 HTML 的所有资源都加载完成后再执行 onload 里面的内容,所有资源包括 DOM 结构、图片、视频 等资源;
- ready 是当 DOM 结构加载完成后就可以执行了,相当于 jQuery 中的 $(function(){ js 代码 });
- 另外,onload 只能有一个,ready 可以有多个。
Object.create(proto)函数
- Object.create()方法创建一个新对象,传入的参数对象为创建的对象的__proto__
- 在创建的对象上改变继承的属性不会对传入的参数对象有影响。
- 返回创建的对象的constructor是ƒ Object()
例子
var a4 = { a: 1 }
var a3 = Object.create(a4);
console.log("a3.__proto__:", a3.__proto__); // Object {a: 1}
console.log(oo.constructor.prototype==Object.prototype)//true
console.log(a3.__proto__ === a3.constructor.prototype); //false
//一般obj.__proto__=obj.constructor.prototype,除了Object.create()创建的对象
闭包
- 什么是闭包:闭包是一个对象,里面保存着内部函数引用的外部函数的变量及变量的值
- 闭包的作用:
- 能够访问函数里的变量
- 能让这些变量一直保存在内存中
function out(x){
return function () {
let ins='inside'
console.log(x)
}
}
let inside=out('xx');
inside();
打断点之后能看到Scopes里的closure对象就是闭包,里面保存着引用的变量

- 形成闭包的条件:只要是嵌套的函数,内部函数引用了外部函数的变量就会形成闭包
堆和栈
- 堆是没有结构的,数据可以任意存放。用于为引用类型的数据分配空间
- 栈是有结构的,主要存放基本数据类型的变量和对象的引用。比如有:函数的参数、局部变量等。
- 栈内存是线性的有序存储,容量小,系统分配效率高
- 存储在堆内存的变量需要先在堆内存中分配存储区域,之后再将地址存储到栈内存中,效率就相对较低。/
- 关于内存的回收:
- 存储在栈内存的变量用完就回收了
- 存储在堆内存的变量不能轻易回收,因为存在不确定的引用,只有当引用的变量为0时才能回收。
如何实现跨域?
在JavaScript中,有一个很重要的安全性限制,被称为同源策略。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下(协议+域名+端口号相同)的内容。

script、image、link都是能跨域的,是发送get请求。在页面上有三种资源是可以与页面本身不同源的。它们是:js脚本,css样式文件,图片
- jsonp:解决AJAX受同源策略的限制的问题,但jsonp只能发送get请求。原理是动态生成script标签,在src属性写上要不同域的路径,然后将回调函数加在路径后面,作为参数传过去。
- WebSocket:是一种双向通信协议,建立连接后,客户端和服务端都能主动向对方发送或接收数据。
- CORS:需要浏览器支持,整个过程由浏览器自动完成,不需用户参与,当发现请求跨域时,会自动添加一些附加的头信息。
- 使用postMessage API(otherWindow.postMessage(message, targetOrigin, [transfer]);)
- document.domain+iframe,只能解决主域相同的跨域问题。使用iframe去加载同一主域的页面,然后用js添加document.domain=该页面的域名或更高一级的父域,在请求的页面也需写上这一属性。
- window.name+代理iframe。当iframe的页面跳转到其他网站时,其window.name值保持不变,但是主页面仍然是不可访问跨域下的iframe的window.name,所以需要一个和主页相同域的代理页面,这样当iframe从请求数据的跨域页面加载完后(监听onLoad事件),再切换成这个代理页面时,就可访问window.name而不会有跨域的问题
- 代理服务器(Node中间件):服务器向服务器请求就无需遵循同源策略。客户端向本地的代理服务器发送请求,代理服务器接收请求,将请求转发给目标服务器,最后将目标服务器的响应结果转发给枯客户端。
- nginx反向代理:实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。(需要下载nginx)
总结:
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
- JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
- 日常工作中,用得比较多的跨域方案是cors和nginx反向代理
CORS
浏览器将CORS请求分为两类:简单请求和非简单请求,对于非简单请求,在正式通信之前,增加一次HTTP查询请求,称为“预检”请求。
简单请求:
1、get、post、head
2、HTTP头信息不超过以下几种字段:Accept、Accept-Language、Content-Language、Content-Type(application/x-www-form-urlencoded 或 multipart/form-data 或 text/plain)
非简单请求:
对服务器有特殊要求的请求,比如put、delete,或者Content-Type类型是application/json
ES6
- 使用export导出数据
- 使用import接收
util.js文件
//1、先定义后导出
let num1=1; let num2=2;
export {num1,num2};
//2、直接导出定义的变量
export var num=1;
export var num2=2;
main.js
//接收
import {num1,num2} from 'util.js'
//可以起别名
import {num1:n1,num2:n2} from 'util.js'
//可以一次性全接收
import * as obj from 'util.js';
console.log(obj.num1,obj.num2);
---------------------------
util.js文件
//3、导出默认数据
export default const obj={}
main.js
//接收
import data from 'util.js'; //可以随意命名一个变量
-------------------------------
//同时导出默认的和部分的,export default只能使用一次
export default const example3 = {
birthday : '2018 09 20'
}
export let name = 'my name'
export let age = 'my age'
export let getName = function(){ return 'my name'}
// 导入默认与部分
import example3, {name, age} from './example1.js'
迭代器与生成器
- 迭代器:迭代器是一个对象,含有next方法,该方法返回一个对象,该对象有done和value属性,value表示迭代到的值,done表示是否迭代结束,当数据迭代完毕,返回true
用ES5实现迭代器的创建
function generator(arr) {
let i=0;
return {
next(){
let done=(i>=arr.length);
let value=!done? arr[i++]:undefined
return{done,value}
}
}
}
let iterator=generator([1,2,3]); //得到迭代器对象
let obj=iterator.next();
while (!obj.done) {
console.log(obj.value);
obj=iterator.next();
}
- 生成器:ES6语法,在函数名前加一个*表示生成器,用来生成一个迭代器对象,函数像正常代码一样编写,函数内部使用yield关键字来指定每次迭代到的数据,当运行到yield函数就会停止向下执行,直到迭代器调用next方法
用ES6的生成器创建迭代器,迭代数组
function *Generator(arr) {
for(let i=0;i<arr.length;i++){
yield arr[i]
}
}
let it=Generator([1,2,3]);
let obj_=it.next();
while (!obj_.done){
console.log(obj_.value);
obj_=it.next()
}
可迭代对象有:数组、字符串、map、set对象,这些都是含有Symbol.iterator属性
commonjs模块(require和module.exports)和ES6模块(import和export)的区别
- commonjs是动态加载,是运行时执行,require可以写在任何地方。获取到的是模块的浅拷贝,可以对模块重新赋值
- es6是静态加载,是编译时加载,import必须写在文件头部,获取到的模块的引用,所以对模块只有只读,不能修改模块指向的地址,对模块重新赋值会编译报错
es6模块
export const person = {
name: 'lc',
friends: ['xm','xh']
};
setTimeout(() => person.name ='liangcheng', 500);
commonjs模块
var dog = {
name: 'xsz', // 不要猜测是谁的名字
owner: ['lc']
};
module.exports = dog;
setTimeout(() => dog.name = 'xushizhou' ,500);
引用模块
import { person } from './a1';
const dog = require('./a2');
setTimeout(() => console.log(person.name, dog.name), 1000); //liangcheng xushizhou
面向对象
三要素:封装、继承、多态
# 1.封装
// 假设需要登记学籍,分别记录小明和小红的学籍号、姓名
let name1 = "小明"
let num1 = "030578001"
let name2 = "小红"
let num2 = "030578002"
// 如果需要登记大量的数据,则弊端会非常明显,而且不好维护,那么我们会使用以下方法来登录,这也是面向对象的特性之一:封装
let p1 = {
name:"小明",
num:"030578001"
}
let p2 = {
name:"小红",
num:"030578002"
}
# 2.继承
// 从已有的对象上,获取属性、方法
function Person(){
this.name = "邵威儒"
}
Person.prototype.eat = function(){
console.log("吃饭")
}
let p1 = new Person()
p1.eat() // 吃饭
let p2 = new Person()
p2.eat() // 吃饭
# 3.多态
// 同一操作,针对不同对象,会有不同的结果
let arr = [1,2,3]
arr.toString() // 1,2,3
let obj = new Object()
obj.toString() // [object Object]
面向过程和面向对象的本质理解
- 面向过程是流程化的,分析解决问题需要几个步骤,用函数把每一个步骤实现,在使用的时候调用函数即可。
- 面向对象是模型化的,抽象出一个类,这是一个封闭的环境,在这个环境中有数据有解决问题的方法,你如果需要什么功能直接使用就可以了,至于是怎么实现的,你不用知道。
总结: 面向对象的底层还是面向过程,面向过程抽象成类,然后封装,方便使用就是面向对象。
ES5的继承和ES6的继承的区别
- ES5的继承实质上是先创建子类的实例,再将父类的方法添加到this上
(Parent.apply(this)) - ES6是先创建父类的实例对象this(通过调用super方法来调用父类的构造函数),然后子类再将自己的属性和方法添加到this上
class Parent{
constructor(){
this.x='parent'
}
}
class Son extends Parent{
constructor(){
var s=super()
console.log(s === this); //true
}
}
var son=new Son()
图片懒加载
- 将页面上的<image>的src属性写成data-src,将图片的路径写在这个下面
- 一开始加载页面就判断图片距离浏览器的顶部的距离是否小于窗口可视高度($img.getBoundingClientRect().top<=window.innerHeight; ),是则将data-src属性赋给src属性
- 然后将已加载的图片设一个标记位,表示已加载过,下次无需再判断
- 然后监听页面的滑动,要使用防抖函数,防止持续的滚动页面从而不断的触发相关函数导致性能损耗。
- 滑动事件监听函数里也是进行前三步的判断
用CSS3动画替代JS模拟动画的好处
好处:
- 不占用js主线程
- 可以采用硬件加速(将浏览器的渲染过程交给GPU处理,可以使得animation和transition更加顺畅)
- 使用3d效果来开启硬件加速:transform3d/rotate3d/scale3d/translateZ
- 浏览器可以对动画进行优化(元素不可见时不动画,减少对FPS的影响)
坏处:
- 浏览器对渲染的批量异步化处理会导致动画难以控制,需要强制同步
$.fn.repaint = function () {
this.each(function (item) {
return item.clientHeight;
});
}
CSS3动画与javascript模拟动画有以下区别:
- CSS 3D动画在js中无法实现
- CSS 2D矩阵动画(指矩阵transform的变化,比如scale、变形、x轴、y轴)效率高于js用margin和left、top模拟的矩阵动画
- CSS3其它常规动画属性的效率均低于js模拟的动画,常规动画属性在这里是指:height,width,opacity,border-width,color…..
JavaScript设计模式
Webpack
-
作用:使前端实现模块化开发,能够使用require与exports,export与import
-
优点:
- 依赖管理:方便引用第三方模块、让模块更容易复用、避免全局注入导致的冲突、避免重复加载或加载不需要的模块。
- 各路插件:babel 把 ES6+ 转译成 ES5 ,eslint 可以检查编译期的错误……
- 合并代码:把各个分散的模块集中打包成大文件,减少 HTTP 的请求链接数,配合 UglifyJS 可以减少、优化代码的体积。
-
原理:一切皆为模块,由于 webpack 并不支持除 .js 以外的文件,从而需要使用 loader 转换成 webpack 支持的模块,plugin插件用于扩展 webpack 的功能,在 webpack 构建生命周期的过程在合适的时机做了合适的事情。
-
webpack 从构建到输出文件结果的过程
- 解析配置参数,合并从 shell 传入和 webpack.config.js文件的配置信息,输出最终的配置信息
- 注册配置中的插件,好让插件监听 webpack 构建生命周期中的事件节点,做出对应的反应
- 解析配置文件中 entry 入口文件,并找出每个文件依赖的文件,递归下去
- 在递归每个文件的过程中,根据文件类型和配置文件中 loader 找出相对应的 loader 对文件进行转换
- 递归结束之后得到每个文件最终的结果,根据 entry 配置生成代码 chunk
- 输出所有 chunk 到文件系统
模块化和组件化
- 组件化从功能出发,强调的是功能性和可复用性
- 模块化从业务逻辑出发,强调的是完整性和业务性
- 好处是:提高代码的复用性,解耦
Vue
数据劫持与监听
vue.js是通过Object.defineProperty以及发布订阅模式来进行数据劫持和监听
v-model的原理
v-model实际是个语法糖,原理是用v-bind绑定表单组件的value/selected/checked,和v-on绑定input/change事件。通常用在表单控件的双向绑定。
自定义组件的v-model如何生效
对于添加了v-model指令的组件(如果含表单控件input),会默认利用value和input事件。但如果是其他类型的表单控件checkbox,就会利用checked和change事件,所以为了通用性,组件将会有model对象属性,定义了prop和event属性来定义绑定的值和事件类型
- 在组件添加v-model指令
<base-checkbox v-model="lovingVue"></base-checkbox>
- 相当于写成
<base-checkbox v-bind:checked="lovingVue" v-on:change="lovingVue=arguments[0]"></base-checkbox>
- 这个组件:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
//checkbox表单控件自己有change事件,它的事件监听函数触发的是这个vue组件的change事件,是父组件绑定的事件,两个change事件是属于不同对象的
>
`
})
综上:父组件的lovingVue传递给子组件,子组件用checked接收,注意在子组件要在props属性显示指出checked这个prop。当子组件的表单控件发生变化,触发父组件绑定的事件,将表单控件的值作为事件函数的参数传递过去,从而改变父组件的lovingVue值。实现子组件更新父组件的值。
computed和watch的区别
相同:两者都是能监听/依赖一个数据,并进行相应的处理
-
computed:类似于过滤器,对绑定到视图的数据进行处理
- 在computed里定义函数,函数被当作属性使用,函数要有返回值
- 具有缓存,只有依赖的属性发生变化时才会重新计算求值
- 不可以执行异步操作
-
watch:watch是一个侦听的动作,用来观察和响应 Vue 实例上的数据变动
- 是以绑定到视图的数据命名的函数(底层其实是里面包裹了一个名为handler的函数)
- 当数据发送变化时,这个响应函数就会被调用
- 这个函数有两个参数,newVal和oldVal
- 可以执行异步操作
- 由于可以执行异步操作,所以在得到最终结果前可以设置中间状态
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
watch执行异步操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的:
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
总结:computed主要用于对同步数据的处理,watch则主要用于观测某个值的变化去完成一段开销较大的复杂业务逻辑。
Vue的单向数据流
- 所有的prop使得父子组件间形成单向下行绑定:父级prop的更新会向下流动到子组件中,因此子组件的prop时时能获取到父组件更新的值。
- 但子组件不允许改变prop,如果想改变可以通过$emit派发一个自定义事件,父组件接收后,由父组件修改。
有两种常见的试图改变一个prop的情形:
- 将prop作为子组件的初始值,并且这个子组件接下来希望将其作为一个本地的prop数据使用:最好定义到本地的data属性
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 这个 prop 以一种原始的值传入且需要进行转换:最好使用这个 prop 的值来定义一个计算属性
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
之前给数组项赋值和给一个空对象添加属性,Vue能检测到变化吗?
不能。
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
虽然Object.defineProperty()可以监听到数组下标的变化,
但是为了性能优化,vue源码并没有使用Object.defineProperty()来劫持数组类型的数据:
如何解决?
- 使用Vue.set(数据,属性/索引,属性值)
- 使用vue.$set(数据,属性/索引,属性值)
- 改变数组长度:vm.items.splice(newLength)
虚拟DOM树-- Virtual DOM 算法
- 有js对象结构表示DOM树的结构,然后利用它构建一个真正的DOM树,再插入到文档中。
- 当状态变更时,用js对象结构再创建一个虚拟的DOM,然后与之前的虚拟DOM进行比较,记下差异的地方
- 把差异应用到真正的DOM树上,视图就更新了
难点:比较两棵虚拟DOM树的差异
比较两棵 DOM 树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。
- 两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动 DOM 元素。
所以 Virtual DOM 只会对同一个层级的元素进行对比:

深度优先遍历,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。
Vue生命周期
-
new一个Vue实例时,实例只有默认的事件和生命周期函数
-
最早能访问到this是:beforeCreate函数
-
在beforeCreate函数之后created之前:data、methods、watch、computed属性进行初始化
-
在created函数能访问data、methods、watch、computed属性,除了el属性
-
在created函数之后,beforeMount函数之前:进行模板编译
- 如果没有el属性,则等到调用vm.$mounted()挂载时再继续往下执行
- 如果没有template属性,则将el的outerHtml内容作为模板template
- 将template编译成render函数
-
beforeMounte函数:此时内存已构建出虚拟的dom树,视图还未更新,不能访问dom上一些动态变量,此时访问el仍是未各种表达式{{}}

-
beforeMounte函数之后:创建vm.$el,与el进行替换
-
Mounted函数:已将虚拟dom挂载到页面上,能访问dom上的动态变量
- 此时已完成实例的初始化阶段,进入运行阶段
- beforeUpdate函数:data已经发生变化触发该函数,视图还未更新
- beforeUpdate函数之后:根据data重新渲染新的虚拟dom,对比新老虚拟dom树,将差异应用到真正的dom树上,完成页面更新
- updated函数:此时页面已是最新的数据
- beforeDestory函数:实例销毁前调用,this仍能获取到实例,常用于销毁定时器、解绑全局事件、销毁插件对象等操作
- destroyed函数:在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

Vue.use()
- 在全局注册/安装组件,把组件挂载到Vue.prototype上,所有组件实例都可访问
组件间传递数据
- props(父向子,当传递的不是变量仅是单纯的字符串时,不需要冒号绑定)
- 父组件在子组件标签上绑定事件 v-on,子组件触发事件$emit(子向父)
- slot:父向子传递‘标签数据’
//父组件
<child>
<div slot="xxx">父组件动态插入数据</div>
<div slot="yyy">父组件动态插入数据</div>
</child>
//子组件
<template>
<div>
<slot name='xxx'></slot>
<div>组件确定的标签结构</div>
<slot name='yyy'></slot>
</div>
</template>
- $refs.名称,获取组件实例
- $parent / $children:访问父 / 子实例
// component-a 子组件
export default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
// 父组件
<template>
<component-a ref="comA"></component-a>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // Vue.js
comA.sayHello(); // 弹窗
}
}
</script>
- bus
- 创建一个js文件,使用export default向外暴露一个对象,对象含有install方法
- 在入口文件main.js引入这个文件,赋给一个变量,比如bus
- 使Vue.use(bus),这个方法会去调用bus的install方法,且会传入Vue作为参数
- 在bus对应的js文件的install函数写:将$bus属性添加到Vue.prototype上,属性值是一个Vue实例(因为为了实现数据更新,视图更新,所以让$bus=Vue实例,利用Vue的data有数据劫持的特点。)
- 将共用的数据添加在data对象(是对象不是方法,因为不是组件,是js文件不是.vue文件)
- 在methods添加on方法、emit方法、off方法
//event-bus.js文件
export default const install=function(Vue){
Vue.prototype.$bus=new Vue({
methods:{
emit(event,...args){
this.$emit(event,...args);
},
on(event,callback){
this.$on(event,callback);
},
off(event,callback){
this.$off(event,callback)
}
}
})
}
在入口文件main.js引入插件
import bus from 'event-bus.js'
Vue.use(bus)
使用$bus绑定事件this.$bus.on()要注意:
1、绑定在哪个生命周期函数,才能确保在其他组件触发事件之前已经绑定好事件,即组件间生命周期函数的执行顺序
2、在组件销毁之前将事件解除,否则当再有组件绑定该事件,添加事件监听函数时,此时事件的监听函数还保留着之前的事件监听函数,即会触发多个回调。
-
vuex
- 安装Vuex
- 新建一个js文件,引入Vuex,使用Vue.use()安装Vuex
- 向外暴露一个Vuex.Store实例
- 向Vuex.Store()构造函数传入四个对象:state、getters、actions(定义的方法可以重名,会按注册的顺序依次触发。可以包含同步/异步操作,用来提交mutation,不直接改变状态state)、mutations(定义的方法不可以重名,只能进行同步操作)
- 在main.js引入这个文件,在Vue实例添加store属性等于这个文件暴露出来的Vuex.Store实例,这样所有组件都有$store属性
- 使用$store.state访问数据
- 使用$store.dispatch(action名,data)调用actions里的方法
- actions里的方法使用commit调用mutations
-
pubsub消息发布/订阅
- 安装pubsub-js
- 接收数据的组件订阅消息
- 传递数据的组件发布消息
//订阅消息
mounted: {
PubSub.subscribe("deleteTodo",(messageName, todosIndex)=>{
this.deleteTodo(todosIndex);
});
//发布消息
methods: {
PubSub.publish("deleteTodo", this.index);
}
- $attrs/$listeners:
多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。
$attrs:包含了从父组件接收的(来自父作用域的),但props没有定义的数据 (class 和 style 除外)。
当一个组件没有声明任何 prop 时,$attrs这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件
这是唯一一次v-bind直接与=相连,一般要传给子组件且props能接收的要v-bind:变量名=数据
$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
总结:
-
父向子传递数据:props,$attrs/$listeners、slot(传递的是标签数据)
-
子向父传递数据:事件绑定和触发$emit事件回调,$ref获取组件实例
-
兄弟间:bus,vuex,pubsub订阅发布消息
-
跨级通信:bus,vuex,pubsub订阅发布消息,$attrs/$listeners
-
任意级别通信:bus、vuex、pubsub订阅发布消息
Vuex的mapState、mapGetters、mapActions
1、state是什么?
- vuex的state和vue中的data有很多相似之处,都是用于存储一些数据或者说是状态值。
- 这些值都将被挂载到数据和dom的双向绑定事件,则当你改变值的时候可以触发dom的更新。
- 虽然state和data有很多相似之处,但state在使用时一般应该挂载到组件的computed计算属性上,这样有利于state的值发生改变时及时响应给子组件(如果你用data去接收$store.state,当然可以接收到值,但由于这只是一个简单的赋值操作,因此state中的状态改变的时候不能被vue中的data监听到,当然你也可以通过watch $store去解决这个问题)
2、mapState辅助函数 mapState是语法糖,不用在computed里多次定义函数,然后返回不同的this.$store.state.数据名,可以直接将store的state直接添加到computed里
- 使用前,引入这个辅助函数
- 语法:mapState( { } / [ ] ),传入对象或数组
import { mapState } from 'vuex'
...
computed: mapState({
count: 'count', // 第一种写法,count为state里定义的,后面的count可以省略
sex: (state) => state.sex, // 第二种写法
from: function (state) { // 用普通函数this指向vue实例,要注意
return this.str + ':' + state.from
},
// 注意下面的写法看起来和上面相同,事实上箭头函数的this指针并没有指向vue实例,因此不要滥用箭头函数
// from: (state) => this.str + ':' + state.from
//也可以在mapState里定义不使用到state的函数
myCmpted: function () {
// 这里不需要state,测试一下computed的原有用法
return '测试' + this.str
}
})
3、...mapState
- ...mapState是ES6语法,展开符
- 当computed已经定义一些函数时,此时发现要引入state,就可以使用展开符将state添加到computed里
computed:{
//原来的继续保留
fn1(){ return ...},
fn2(){ return ...},
fn3(){ return ...}
......
//再维护vuex
...mapState( //这里的...不是省略号了,是对象扩展符
[ count ]
)
}
自定义指令
语法:
- 全局指令:
Vue.directive('指令名',{
bind(el,binding,vnode){
//一开始绑定时调用,只调用一次
//el:绑定指令的dom元素,binding传递的是绑定的值,存储的是对象
},
update(el,binding,vnode){
//绑定的数据发生更新时调用,0-多次
}
})
<p v-focus:top="10"></p>
Vue.directive('focus',{
bind(el,binding,vnode){
el.style[binding[arg]]=binding[value]+'px'
},
update(el,binding,vnode){
//绑定的数据发生更新时调用,0-多次
}
})
- 私有指令:在Vue实例里定义directives属性
directives:{
focus:{
binding(el,binding){
}
}
}
v-for添加key属性
当Vue用 v-for 正在更新已渲染过的元素列表是,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue将不是移动DOM元素来匹配数据项的改变,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
- 因为vue组件高度复用组件,增加Key可以标识组件的唯一性,更好地区别各个组件
- key的作用主要是为了高效的更新虚拟DOM diff算法
$router与$route的区别
- $router是VueRouter的一个实例,是通过安装并引入VueRouter,安装VueRouter —— Vue.use(VueRouter)和new VueRouter({})得到的,是一个全局对象
- $router.push({path:'/home'}):使用js方式代替切换路由,可以回退到上一个
- $router.replace({path:'/home'}):替换路由,没有历史记录
- $route是局部对象,指定当前路由,$route有path(绝对路径)、params、query、name等属性
vue-router会出现的相关问题
1、当使用路由参数切换路由,实现同一个路由路径+不同的参数来展示不同的内容,即:前后都是同一组件,但显示的内容不一样
- 原因:原来的组件会被复用,因为两个路由都渲染同一组件,比起销毁再创建,复用则显得更加高效,这意味着组件的生命周期函数不会再调用
- 解决:在当前组件监视路由的变化
watch:{
$route(to,from){
//to:就是切换后的路由
//根据需求更改数据
}
}
2、使用路由切换组件时,一切换之前的组件就会被销毁,再次切换到该组件时,之前的状态不会保存,因为是再次创建的
- 解决:使用<keep-alive>包裹,实现组件缓存。但是要根据需求决定是否缓存组件。
vue-router的导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。 有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
展示一种全局的,更多的查看链接:
- 全局前置守卫:router.beforeEach:当切换路由时调用,可以根据相关逻辑判断是否放行(实现跳转)
路由的元信息
定义路由的时候可以配置 meta 字段,属性值是对象
meta:可以与上面的导航守卫搭配使用,比如切换到网站的主页路由时,需要用户登录才可成功跳转,显示组件,那就在该路由加配置meta字段,当跳转路由时,就会触发router.beforeEach钩子函数,在里面进行对路由的meta里自定义的一些标志位的判断,再决定是否放行。
SPA单页面
SPA:仅在Web页面初始化时加载相应的HTML、JavaScript、CSS,加载完成后,不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用路由机制来实现HTML内容的变换,避免页面的重新加载。
-
优点:
- 用户体验好、快:内容改变时不用重新加载整个页面,避免了不必要的跳转和重复渲染。
- 基于上面一点,SPA对服务器压力小
-
缺点:
- 初次加载耗时多,得等到Vue编译后的JS文件下载完后才能渲染
- SEO难度大:由于所有内容都在一个页面中动态地替换显示
Vue SSR
- Vue.js是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。
- SSR是指在服务端将组件渲染成html片段然后返回给浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务端渲染SSR的优缺点:
- 优点:
- 更好的SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面。
- 首屏加载速度更快: SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
- 缺点:
- 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数;服务端渲染应用程序,需要处于 Node.js server 运行环境;
- 更多的服务器负载
MVVM
- view层:视图层,由HTML和css构建
- model层:数据模型,保存每个页面中单独的数据
- viewModel层:视图模型层,是v与m的桥梁,起承上启下的作用。
- 向上于视图层进行双向数据绑定,向下与模型层通过接口请求的方式进行数据交互。
- vm将获取到的数据进行处理转换,做二次封装,以生成符合view层的数据模型。这样view层展现的不是model层的数据,而是vm层的数据。
- 双向数据绑定使前端开发者不必低效又麻烦地去操作dom去更新视图。
MVVM和MVC的区别
- MVC:是后端分层开发的概念。V层接收用户的输入操作,C层响应view的事件,当涉及到数据的增删改查曾调用model层的接口对数据进行操作,model层数据变化后通知v层更新视图。
- MVVM:与MVC最大的区别就是实现了m层与v层的自动同步,vm层是m层和v层的桥梁,与v层进行双向数据绑定,与m层通过接口的形式进行数据交换。开发者不用手动操作dom就能实现v层的更新。(在vm层直接修改数据即可实现v层的更新)
Vue如何实现数据的双向绑定
- view -- > data变化:通过事件监听的方式
- data --> view变化:数据劫持与监听、订阅发布模式
- Object.defineProperty()为data对象的每层属性添加getter和setter,当数据发生改变,就会触发setter,从而监听到数据的变化。实现数据的劫持与监听(Observer)
- 当在遍历每一层属性时,在每一层都实例化一个Dep实例--订阅器,用于存放订阅者,当某个属性发生变化,该层的Dep实例就会发布消息,让Dep实例上的所有订阅者去更新视图。
- 订阅者的来历:先编译模板(Compile),将指令替换成数据显示,每一个指令都实例出一个Watcher实例(订阅者),Watcher实例含更新视图的函数,在Watcher的构造函数里,会去访问data的属性,从而调用getter,因此在getter里找到该层属性的Dep实例,将Watcher实例添加到Dep实例里,实现了订阅。
- Watcher是Dep和Compile的桥梁
- Dep是Observer和Watcher的桥梁

vue为什么不能监听数组的变化
在vue中,不能检测到以下对数组的操作:
- arr[index]=newVal
- arr.length=5
原因?
- vue是通过Object.defineProperty实现数据劫持的,给属性添加getter和setter
- 但Object.defineProperty也能将数组的索引当初属性来添加getter和setter
- 但出于对性能的考虑,vue的源码只对是对象类型的数据使用Object.defineProperty
vue只能监听以下八种对数组操作的方法:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
Object.defineProperty() 和 proxy 的区别
vue中为了对对象的每层属性都实现监听,需要不断遍历对象的属性,使用Object.defineProperty()来实现数据劫持,则Object.defineProperty()只能劫持数据的属性,如果能直接劫持一个完整的对象而非对象上的属性,则无需层层遍历属性了。
Proxy:
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var proxy=new Proxy(target,handler)
参数:
- target:是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
- handler:一个对象,定义了一些函数,用于拦截对应的操作。比如,定义一个get方法,用来拦截对目标对象属性的访问请求。
返回值:
- 返回代理对象,直接操作该对象来达到操作目标对象的效果
- 如果直接操作目标对象不能达到Proxy的效果
let obj = {};
let handler = {
get(target, property) {
console.log(`${property} 被读取`);
return property in target ? target[property] : 3;
},
set(target, property, value) {
console.log(`${property} 被设置为 ${value}`);
target[property] = value;
}
}
let p = new Proxy(obj, handler);
p.name = 'tom' //name 被设置为 tom
p.age; //age 被读取 3
p 读取属性的值时,实际上执行的是 handler.get() :在控制台输出信息,并且读取被代理对象 obj 的属性。
p 设置属性值时,实际上执行的是 handler.set() :在控制台输出信息,并且设置被代理对象 obj 的属性的值。
handler可以定义多种拦截的函数:Proxy
综上:取代Object.defineProperty()的Proxy有以下两个优点
能直接劫持整个对象,无需通过递归和遍历data对象来实现对数据的监控
有多种劫持操作
Webpack
模块化解决了前端的哪些痛点
- 代码复用
- 命名冲突
- 文件依赖
webpack 的 loader 和 plugin 区别
- loader用于对模块中的源代码进行转换。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件! 因为 webpack 本身只能处理 JavaScript,如果要处理其他类型的文件,就需要使用 loader 进行转换,loader 本身就是一个函数,接受源文件为参数,返回转换的结果。
- Plugin 是用来扩展 Webpack 功能的。使用 plugin 丰富的自定义 API 以及生命周期事件,可以控制 webpack 打包流程的每个环节,实现对 webpack 的自定义功能扩展。plugin能监听webpack构建生命周期的事件节点。