按顺序执行
//例子:
console.log(1)
console.log(2)
console.log(3)
1 2 3
先执行一部分,然后等待结果,等拿到结果之后,再执行另一部分
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
一个任务完成之后,才能执行另外一个任务
不管定时器写几,也是在同步的代码完成之后,再执行异步的代码
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
执行的时间很长
process.nextTick方法只能在node中运行,在同步代码执行完成之后,异步代码执行之前完成的
//例子:
process.nextTict(()=>{
console.log(1)
})
console.log(2)
setTiemout(function(){console.log(3)},0)
console.log(4)
执行顺序:2 4 1 3
异步代码执行之后,执行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
同步的代表会放到运行栈中执行
异步
检测任务队列里面有没有东西,如果有一个任务,他就执行,如果有多个任务,他就会按顺序执行
注解:
定时器不是到点执行,而是到点了之后插到任务队列里头,而任务队列什么时候执行,那需要看运行栈里面是否执行完,运行栈执行完了,要看任务队列里前面有没有任务,有任务的话,需要执行完,才执行刚刚插入的定时器里头的任务,所以为什么定时器为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(当前事件循环结束执行)
每次事件循环都看任务队列里面有没有东西,有就执行
计时器,ajax,读取文件
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
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里面的形参
async函数调用之后,他的的返回值是promise对象, promise对象的then传过来的参数,就是return的值
可以理解为,async函数就是promise对象的简写
async function fun(){
return 1
}
let a = fun()//fun是promise对象,要想拿到这个返回值1,则需要使用then方法
console.log(a)//打印出 Promise {: 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
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中,作用域一共分为三类:全局作用域、局部(函数)作用域、块级作用域。
靠里作用域可以访问到定义在靠外的作用域的变量,反之不行。
var age = 14 //全局作用域,全局任何地方都可以访问到
function fun(){
var name = 'xiaoming' // 局部作用域/函数作用域
}
console.log(age) // 14
console.log(name) // 报错 name is not defined
块级作用域是ES6新推出的概念,用let和const关键字声明变量。所有{}都会形成独立的块级作用域,例如if、for,注意对象的{}不包括在内。
{
var pppp = 3
let ooo = 2
const iii = 4
}
console.log(pppp) // 3
console.log(iii) // 报错
console.log(ooo) // 报错
function consoleAge(age){
console.log('今年' + age +'岁')
}
consoleAge(18) // 今年18岁
consoleAge(age) // 报错
// 使用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 {
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) // 报错
在编译阶段要确定作用域首先要做的就是找到所有变量的声明,并利用相关机制将它们关联起来 ,在构建作用域时,会首先将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只能声明一次?
因为var存在变量提示,就算多个变量声明,也是按照最后一个为准。
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];
}
arr.forEach(function(e){
});
for(let j in arr) {
}
arr.map(function(n){
});
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 |
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的作用都是改变函数运行时this的指向。其次,在 ES6 的箭头函数下, call 和 apply、bind 将失效。
任何情况下直接在script中写入的this都是window。
函数中的this 非严格模式:this指向window, 严格模式时:this指向undefined。
箭头函数的this
this都指向箭头函数外上下文环境的this指向
对象中this
对象属性的this 指向对象外上下文环境的this
对象方法(普通函数)中的this,指向当前对象(谁执行该方法,this就指向谁)
Fun.call(obj,'arg1', 'arg2')
Fun.apply(obj,['arg1', 'arg2'])
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
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现;也就是说,闭包可以让你在一个内层函数中访问到其外层函数的作用域(也通常被称为是函数之间的沟通的桥梁)。
首先:hash和history两种路由模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由;
hash通过监听浏览器的onhashchange()事件变化,查找对应的路由规则
history原理: 利用H5的 history中新增的两个API pushState() 和 replaceState() 和一个事件onpopstate监听URL变化
注意:history模式URL就要和后端进行一致,所以要改为history也需要后端的配合,否则会报错。所以hash模式在每次刷新页面时是直接更改“#”后的东西,history每次刷新会重新像后端请求整个网址,也就是重新请求服务器。如果后端没有及时响应,就会报错404!。history的好处是可以进行修改历史记录,并且不会立刻像后端发起请求。不过如果对于项目没有硬性标准要求,我们可以直接使用hash模式开发。
我们知道通过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__属性。