文章目录
一、js执行机制
1.同步与异步
(1).同步
按顺序执行
//例子:
console.log(1)
console.log(2)
console.log(3)
1 2 3
(2).异步
先执行一部分,然后等待结果,等拿到结果之后,再执行另一部分
1.定时器
2.ajax
3.读取文件
解释:同步程序完成后,后执行异步程序
//例子1:
console.log(1)
setTiemout(function(){console.log(2)},0)
setTiemout(function(){console.log(3)},0)
setTiemout(function(){console.log(4)},0)
console.log(5)
//打印出 1 5 2 3 4
//例子2:
setTiemout(function(){console.log(1)},1000)
setTiemout(function(){console.log(2)},100)
setTiemout(function(){console.log(3)},10)
//打印出 321
2.单线程
一个任务完成之后,才能执行另外一个任务
不管定时器写几,也是在同步的代码完成之后,再执行异步的代码
for(var i = 0;i<2000;i++){
console.log(1)
}
setTiemout(function(){console.log(2)},0)
setTiemout(function(){console.log(3)},0)
setTiemout(function(){console.log(4)},0)
console.log(5)
打印出 先输出2000个1,5 2 3 4
执行的时间很长
3.process.nextTick与setImmediate方法
(1).process.nextTick
process.nextTick方法只能在node中运行,在同步代码执行完成之后,异步代码执行之前完成的
//例子:
process.nextTict(()=>{
console.log(1)
})
console.log(2)
setTiemout(function(){console.log(3)},0)
console.log(4)
执行顺序:2 4 1 3
(2).setImmediate
异步代码执行之后,执行setImmediate
//例子:
setImmediate(()=>{
console.log(1)
})
process.nextTict(()=>{
console.log(2)
})
console.log(3)
setTiemout(function(){console.log(4)},0)
console.log(5)
执行顺序:3 5 2 4 1
4.事件循环
(1).运行栈
同步的代表会放到运行栈中执行
(2). 任务队列
异步
(3).事件循环
检测任务队列里面有没有东西,如果有一个任务,他就执行,如果有多个任务,他就会按顺序执行
注解:
定时器不是到点执行,而是到点了之后插到任务队列里头,而任务队列什么时候执行,那需要看运行栈里面是否执行完,运行栈执行完了,要看任务队列里前面有没有任务,有任务的话,需要执行完,才执行刚刚插入的定时器里头的任务,所以为什么定时器为0还没执行他的原因
//例子
setImmediate(()=>{
console.log(1)
})
process.nextTict(()=>{
console.log(2)
})
console.log(3)
setTiemout(function(){console.log(4)},0)
setTiemout(function(){console.log(5)},1000)
setTiemout(function(){console.log(6)},0)
console.log(7)
==执行顺序:
1.同步
2.nextTick
3.异步
4.setImmediate(当前事件循环结束执行)
每次事件循环都看任务队列里面有没有东西,有就执行==
执行顺序:3 7 2 4 6 1 5
执行顺序:
1.同步
2.process.nextTick
3.异步
4.setImmediate(当前事件循环结束执行)
每次事件循环都看任务队列里面有没有东西,有就执行
5.宏任务和微任务
(1) 宏任务
计时器,ajax,读取文件
(2) 微任务
promise.then
执行顺序:
1.同步程序
2.process.nextTick
3.微任务
4.宏任务
5.setImmediate
//题目:
setImmediate(()=>{
console.log(1)
})
console.log(2)
setTiemout(function(){console.log(3)},0)
setTiemout(function(){console.log(4)},100)
console.log(5)
new Promise((resolve)=>{
console.log(6)//promise.then的这个6是同步代码
resolve()
}).then(()=>{{
console.log(7)
})
process.nextTict(()=>{
console.log(8)
})
//执行顺序:2 5 6 8 7 3 1 4
6.promise和async函数
(1).promise函数
promise的基本概念:then的用法就是通过resolve传出来的 resolve传出来的值,是then里面的形参
*** 以下案例解:就这样运行,他不会打印出2,因为new Promise函数里面会有一个resolve,只有调用了resolve才会执行then***
let p = new Promise(()=>{
console.log(1)//同步
})
p是promise对象,promise对象又一个then方法
p.then(()=>{
console.log(2)//异步
})
*** 修改上面案例***
let p = new Promise((resolve)=>{
console.log(1)//同步
resolve('hello word')
})
p是promise对象,promise对象又有一个then方法
p.then((data)=>{
console.log(data)//异步
})
//执行:打印出 1 hello word
//例子:
ajax.get('').then((res)=>{
//其实get方法他的返回值就是一个promise对象
})
ajax.get('')是怎么封装的,他会把一个获取到的远程数据通过resolve方法传出来,然后才能调用then()拿到这个数据
promise的基本概念:then的用法就是通过resolve传出来的
resolve传出来的值,是then里面的形参
(2).async 函数
async函数调用之后,他的的返回值是promise对象, promise对象的then传过来的参数,就是return的值
可以理解为,async函数就是promise对象的简写
async function fun(){
return 1
}
let a = fun()//fun是promise对象,要想拿到这个返回值1,则需要使用then方法
console.log(a)//打印出 Promise {<fulfilled>: 1}
a.then((data)=>{
console.log(data)//执行后,打印出1
})
async函数调用之后,他的的返回值是promise对象,promise对象的then传过来的参数,就是return的值
可以理解为,async函数就是promise对象的简写
------------------------------------------------
async function fun(){
return 1
}
这个写法可以换成
function fun(){
return new Promise((resolve)=>{
resolve(1)
})
}
fun.then((data)=>{
console.log(data)//执行后打印出 1
})
方法是一样的,只是async函数看上去会顺畅很多,代码不会写的太多
--------------------------------------------------------------
还可以将方法换成:
let p1 = new Promise((resolve)=>{
resolve(1)
})
let p2 = new Promise((resolve)=>{
resolve(2)
})
//获取到1,2的简写
async function fun(){
let a = await p1;//a相当于 await 后面加一个promise对象.await+promise对象他就可以直接拿到resolve的值了,这就是async的用法,
let b = await p2;
console.log(a)//打印出1
console.log(b)//打印出2
}
async函数里面可以加await,await后面可以加promise对象,然后就可以让异步的代码,写起来更像同步的代码
-----------------------------------------------------------------
题目:
async function fun1(){
let data = await fun2()
console.log(data)//异步
}
async function fun2(){
console.log(200)//同步的代码
return 100
}
fun1()//打印出 200 100
7.示例
console.log(1)
async function async1(){
await async2()
console.log(2)
}
async function async2(){
console.log(3)
}
async1()
setTimeout(function(){
console.log(4)
},0)
new Promise(resolve=>{
console.log(5)
resolve()
}).then(function(){
console.log(6)
}).then(function(){
console.log(7)
})
console.log(8)
执行顺序:????????
二、js作用域
(1).作用域分类
在js中,作用域一共分为三类:全局作用域、局部(函数)作用域、块级作用域。
(2).全局作用域、局部作用域(var声明)
靠里作用域可以访问到定义在靠外的作用域的变量,反之不行。
var age = 14 //全局作用域,全局任何地方都可以访问到
function fun(){
var name = 'xiaoming' // 局部作用域/函数作用域
}
console.log(age) // 14
console.log(name) // 报错 name is not defined
(3).块级作用域(let、const声明)
块级作用域是ES6新推出的概念,用let和const关键字声明变量。所有{}都会形成独立的块级作用域,例如if、for,注意对象的{}不包括在内。
{
var pppp = 3
let ooo = 2
const iii = 4
}
console.log(pppp) // 3
console.log(iii) // 报错
console.log(ooo) // 报错
(4).作用域中的特殊情况
- 函数参数——属于函数(局部)作用域
function consoleAge(age){
console.log('今年' + age +'岁')
}
consoleAge(18) // 今年18岁
consoleAge(age) // 报错
- for循环
// 使用var—全局作用域,外面也可以访问
for (var i = 0; i < 10; i++) {
console.log(i)
}
console.log(i) // 10
// 使用let—块级作用域,只能在{}范围内被访问,外面访问不到
for (let j = 0; j < 10; j++) {
console.log(j)
}
console.log(j) // 报错
- try…catch中的err—不属于任何作用域,只能被catch访问
try {
throw new Error('错误内容')
} catch (error) {
var a1 = '全局变量'
let a2 = '块级变量'
const a3 = '块级变量'
console.log(error) // 错误内容
}
console.log(a1) // 全局变量
console.log(a2) // 报错
console.log(a3) // 报错
console.log(error) // 报错
(5).作用域的特点
- 变量提升(预处理)
在编译阶段要确定作用域首先要做的就是找到所有变量的声明,并利用相关机制将它们关联起来 ,在构建作用域时,会首先将var,function声明的变量或函数,进行变量提升处理,就是说提前声明。
a()
function a(){
console.log(b) //undefined
var b=10
}
var a = undefined
- 补充:
| 概念 | 定义 |
| --- | :--- |
| 声明 | var a; 代表声明,现在的a还是undefined,但是告诉了编辑器存在a这个变量 |
| 赋值 | a=10; 对已经存在的变量存取值 |
- 通过var定义的变量会提升,而let和const进行的声明不会提升
- 通过function关键字定义的函数会被提升,而函数表达式则不会提升。
- 变量提示只是提升声明位置,并不会提升赋值,函数会提升整个函数
- 函数的提升大于变量的提升
- 对于同一个变量的多次提升,以最后一次为准备
扩展:为什么var变量可以声明多次,而let const只能声明一次?
因为var存在变量提示,就算多个变量声明,也是按照最后一个为准。
三、js中几种常用的循环区别及性能
1.普通for循环
for (let i = 0; i < arr.length; i++) {
let obj = arr[i];
}
优化:(len = arr.length,减少了每次循环的arr.length的查找,赋值为变量len,后面直接用于比较)
for (let i = 0,len = arr.length; i < len; i++) {
let obj = arr[i];
}
2.foreach 数组方法
arr.forEach(function(e){
});
3.for in 遍历对象
for(let j in arr) {
}
4.map遍历(es6)
arr.map(function(n){
});
5.for of(es6)
for(let value of arr) {
}
区别:
循环 | 数据结构 | 特点 | 性能 |
---|---|---|---|
for | 数组 | 1. 配合continue,break终止循环 2. 书写繁琐 | 1 |
forEach | 数组 | 1. 利用return实现continue效果 2. 没有break功能 | 2 |
for…of | 实现了Iterator接口的数据结构,比如:数组,map,set | 1. 配合continue,break终止循环 | 3 |
for…in | 对象 | 1. 遍历对象返回的对象的key值,遍历数组返回的数组的下标(key) 2. 循环不仅可以遍历数字键名,还会遍历原型上的值和手动添加的其他键 3. 特别情况下, 会以看起来任意的顺序遍历键名 4. 配合continue,break终止循环 5. key值为字符串,其它类型会转化为字符串 | 4 |
性能对比:(for >forEach>map>for…of>for…in)
const data = new Array(40000000).fill(0);
let len = data.length;
// for
const forStart = new Date();
for(let i = 0; i < len; i++){};
const forEnd = new Date();
const for1Start = new Date();
for (let i = 0,len = data.length-1; i < len; i++) {}
const for1End = new Date();
// forEach
const forEachStart = new Date();
data.forEach( item => {});
const forEachEnd = new Date();
// for...of
const forOfStart = new Date();
for(let item of data){}
const forOfEnd = new Date();
// for...in
const forInStart = new Date();
for(let index in data){};
const forInEnd = new Date();
// map
const mapStart = new Date();
data.map(item=>{item})
const mapEnd = new Date();
console.log('for 耗时: ', forEnd - forStart); // for 耗时: 55
console.log('for优化 耗时: ', for1End - for1Start); // for优化 耗时: 39
console.log('forEach 耗时: ', forEachEnd - forEachStart); // forEach 耗时: 468
console.log('map 耗时: ', mapEnd - mapStart); // map 耗时: 533
console.log('for...of 耗时: ', forOfEnd - forOfStart); // for...of 耗时: 627
console.log('for...in 耗时: ', forInEnd - forInStart); // for...in 耗时: 24966
四、call、apply和bind的区别
首先call、apply、bind的作用都是改变函数运行时this的指向。其次,在 ES6 的箭头函数下, call 和 apply、bind 将失效。
(1).对this理解
-
任何情况下直接在script中写入的this都是window。
-
函数中的this 非严格模式:this指向window, 严格模式时:this指向undefined。
-
箭头函数的this
this都指向箭头函数外上下文环境的this指向 -
对象中this
对象属性的this 指向对象外上下文环境的this
对象方法(普通函数)中的this,指向当前对象(谁执行该方法,this就指向谁)
(2).参数区别
- call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表,当第一个参数为null、undefined的时候,默认指向window。
Fun.call(obj,'arg1', 'arg2')
- apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。
Fun.apply(obj,['arg1', 'arg2'])
- bind接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组,这一点与apply相同。
(3).调用区别
- call、apply都是立即调用。bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数,便于稍后调用。而原函数 中的 this 并没有被改变,依旧指向原来该指向的地方
- bind应用场景:给参数指定默认参数、绑定构造函数
function fn(a, b, c) {
console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dog');
fn('A', 'B', 'C'); // A B C
fn1('A', 'B', 'C'); // Dog A B
fn1('B', 'C'); // Dog B C
fn.call(null, 'Dog');
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
五、闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现;也就是说,闭包可以让你在一个内层函数中访问到其外层函数的作用域(也通常被称为是函数之间的沟通的桥梁)。
1.闭包的3个特性
- 函数嵌套函数;
- 函数内部可以引用函数外部的参数和变量;
- 参数和变量不会被垃圾回收机制回收。
2. 闭包的作用、坏处及应用
1.作用
- 保护函数内部的变量安全,实现封装,防止变量流入其他环境发生命名冲突;
- 在内存中维持一个变量,可以缓存(但消耗内存,影响网页性能);
- 匿名自执行函数可以减少内存消耗。
2.坏处:
- 被引用的私有吧变量不能被销毁,增加了内存消耗,造成内存泄漏——> 解决:使用完变量后手动为它赋值为null;
- 闭包涉及跨域访问,会导致性能损失——>解决:通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。
3.应用
- for循环中的保留i 的操作;
- 防抖和节流;
- 函数柯里化;
- 设计模式中的单例模式。
六、hash和history的区别
首先:hash和history两种路由模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由;
1.区别
- hash 就是指 url 后面的 # 号以及后面的字符,history没有带#,外观上比hash 模式好看些
- 原理的区别(原理)
- hash 能兼容到IE8, history 只能兼容到 IE10;
- 由于 hash 值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件(hashchange只能改变 # 后面的url片段);虽然hash路径出现在URL中,但是不会出现在HTTP请求中,对后端完全没有影响,因此改变hash值不会重新加载页面,基本都是使用 hash 来实现前端路由的。
2.原理
-
hash通过监听浏览器的onhashchange()事件变化,查找对应的路由规则
-
history原理: 利用H5的 history中新增的两个API pushState() 和 replaceState() 和一个事件onpopstate监听URL变化
-
注意:history模式URL就要和后端进行一致,所以要改为history也需要后端的配合,否则会报错。所以hash模式在每次刷新页面时是直接更改“#”后的东西,history每次刷新会重新像后端请求整个网址,也就是重新请求服务器。如果后端没有及时响应,就会报错404!。history的好处是可以进行修改历史记录,并且不会立刻像后端发起请求。不过如果对于项目没有硬性标准要求,我们可以直接使用hash模式开发。
七、vue如何监听到数组的变化的
我们知道通过Object.defineProperty()劫持数组为其设置getter和setter后,调用的数组的push、splice、pop等方法改变数组元素时并不会触发数组的setter,这就会造成使用上述方法改变数组后,页面上并不能及时体现这些变化,也就是数组数据变化不是响应式的(对上述不了解的可以参考这篇文章)。但实际用vue开发时,对于响应式数组,使用push、pop、shift、unshift、splice、sort、reverse等方法改变数组时,页面会及时体现这种变化,那么vue中是如何实现的呢?
// src/core/observer/array.js
// 获取数组的原型Array.prototype,上面有我们常用的数组方法
const arrayProto = Array.prototype
// 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 列出需要重写的数组方法名
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
methodsToPatch.forEach(function (method) {
// 保存一份当前的方法名对应的数组原始方法
const original = arrayProto[method]
// 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
def(arrayMethods, method, function mutator (...args) {
// 调用数组原始方法,并传入参数args,并将执行结果赋给result
const result = original.apply(this, args)
// 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 将当前数组的变更通知给其订阅者
ob.dep.notify()
// 最后返回执行结果result
return result
})
})
从上面可以看出array.js中重写了数组的push、pop、shift、unshift、splice、sort、reverse七种方法,重写方法在实现时除了将数组方法名对应的原始方法调用一遍并将执行结果返回外,还通过执行ob.dep.notify()将当前数组的变更通知给其订阅者,这样当使用重写后方法改变数组后,数组订阅者会将这边变化更新到页面中。
重写完数组的上述7种方法外,我们还需要将这些重写的方法应用到数组上,因此在Observer构造函数中,可以看到在监听数据时会判断数据类型是否为数组。当为数组时,如果浏览器支持__proto__,则直接将当前数据的原型__proto__指向重写后的数组方法对象arrayMethods,如果浏览器不支持__proto__,则直接将arrayMethods上重写的方法直接定义到当前数据对象上;当数据类型为非数组时,继续递归执行数据的监听。
// src/core/observer/index.js
export class Observer {
...
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
...
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
对于响应式数组,当浏览器支持__proto__属性时,使用push等方法时先从其原型arrayMethods上寻找push方法,也就是重写后的方法,处理之后数组的变化会通知到其订阅者,更新页面,当在arrayMethods上查询不到时会向上在Array.prototype上查询;当浏览器不支持__proto__属性时,使用push等方法时会先从数组自身上查询,如果查询不到会向上再Array.prototype上查询。
对于非响应式数组,当使用push等方法时会直接从Array.prototype上查询。
值得一提的是源码中通过判断浏览器是否支持__proto__来分别使用protoAugment和copyAugment 方法将重写后的数组方法应用到数组中,这是因为对于IE10及以下的IE浏览器是不支持__proto__属性。