1.执行上下文和执行栈
执行上下文
什么是执行上下文?
Javascript 代码都是在执行上下文中运行的
执行上下文生命周期
1)创建阶段
生成变量对象,建立作用域链,确定this指向
2)执行阶段
变量赋值、函数引用、执行其他代码
执行上下文的特点:
1)单线程,只在主线程上运行
2)同步执行,从上向下按顺序执行;
3)全局上下文只有一个,也就是window对象;
4)函数每调用一次就会产生一个新的执行上下文环境。
执行栈【调用栈-后进先出】
是一种先进后出的数据结构,用来存储代码运行的所有执行上下文
1.当js引擎,第一次遇到js脚本时,会创建一个全局的执行上下文并压入当前的执行栈
2.当js引擎遇到第一个函数调用,它会为该函数创建一个新的执行上下文并压入栈顶部
3.当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文
4.一旦所有代码执行完毕,JS 引擎从当前栈中移除全局执行上下文
var a = 1; // 1. 全局上下文环境
function bar(x) {
console.log("bar");
var b = 2;
fn(x + b); // 3. fn上下文环境
}
function fn(c) {
console.log(c);
}
// 2. bar上下文环境
bar(3);
运行结果:bar 5
2.作用域
定义:可访问变量的集合
优点:隔离变量,不同作用域下同名的变量不会冲突
作用域的类型:
全局作用域、函数作用域、ES6新增了块级别作用域
函数作用域
声明在函数内部的变量
块作用域
1)块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
2)在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问
var、let、const的区别
var定义的变量,
没有块级的概念,可以跨块访问,可以变量提升let定义的变量,
只能在块作用域里面访问,不能跨块访问,无法变量提升,不可重复const定义常量,
使用时候必须初始化,只能在块作用域里面访问,不能修改,没有变量提升,不能重复
var与let的经典案例
//用let声明i,for循环体内部是一个单独的块级作用域,相互独立,不会相互覆盖
var a = []
for(let i = 0;i < 10;i++){
a[i] = function(){
console.log(i)
}
}
a[0]();
输出结果是:0
//i是var声明的,在全局范围内都有效,全局只有一个变量i,输出的是最后一轮的i值,也就是 10
var a = []
for(var i = 0;i < 10;i++){
a[i] = function(){
console.log(i)
}
}
a[0]();
输出结果是:10
作用域链
当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做
作用域链
3.this
this的5种绑定
- 默认绑定(非严格模式下this指向全局对象,严格模式下函数内的this指向
undefined) - 隐式绑定(当函数引用有上下文对象时, 如
obj.foo()的调用方式, foo内的this指向obj) - 显示绑定(通过call或者apply方法直接指定this的绑定对象, 如
foo.call(obj)) - 显示绑定(通过call或者apply方法直接指定this的绑定对象, 如
foo.call(obj)) - 箭头函数,不能改变,箭头函数内的this是由外层作用域决定的
案例1:严格模式的this指向
严格模式,函数内部的this指向undefined
"use strict";
var a = 10;
function foo() {
console.log("this1", this);
console.log("this2", window.a);
console.log("this3", this.a);
}
console.log("this4", this);
foo();
严格模式
非严格模式
案例2:let和const
var 改成了 let 或 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined
let a = 10;
const b = 20;
function foo() {
console.log(this.a); // undefined
console.log(this.b); // undefined
}
foo();
console.log(window.a); // undefined
案例3:函数调用
foo()函数内的this指向的是window,因为是window调用的foo,打印出的this.a是window下的a
var a = 1
function foo () {
var a = 2
console.log(this) // window
console.log(this.a) // 1
}
foo()
案例4:箭头函数
它的this由外层作用域决定
var obj = {
name: "obj",
foo1: () => {
console.log(this.name); // window
},
foo2: function () {
console.log(this.name); // obj
return () => {
console.log(this.name); // obj
};
},
};
var name = "window";
obj.foo1();
obj.foo2()();
4.闭包
要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
闭包的垃圾回收:
不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收
经典案例展示
for(var i = 0; i < 10;i++){
setTimeout(()=>{
console.log(i)
},1000)
}
输出结果:10
for(let i = 0; i < 10;i++){
setTimeout(()=>{
console.log(i)
},1000)
}
输出结果:0 1 2 3 4 5 6 7 8 9
5.原型和原型链
原型的作用:给其他对象提供共享属性的对象,函数的实例可以共享原型上的属性和方法
原型链:
当你访问一个对象上的变量的时候,如果该对象内部不存在这个属性,那就会去它的__proto__属性所指向的对象(原型对象)上查找。如果原型对象依旧不存在这个属性,就去原型的的__proto__属性所指向的原型对象上去查找,以此类推,直到找到null,就构成了一个原型链
原型链和作用域的区别: 原型链是查找对象上的属性,作用域链是查找当前上下文中的变量
instanceof 判断对象是否为该构造函数的实例
instanceof 的基本用法,它可以判断一个对象的原型链上是否包含该构造函数的原型,经常用来判断对象是否为该构造函数的实例
console.log(Object instanceof Object); //true
console.log(Function instanceof Function); //true
console.log(Function instanceof Object); //true
console.log(function() {} instanceof Function); //true
instanceof与typeof的区别
1.
typeof一般被用于来判断一个变量的类型 typeof可以用来判断number、undefined、string、boolean特殊情况:
typeof null === 'object'typeof [] === 'object'typeof {} === 'object'2)
instanceof判断一个对象的原型链上是否包含该构造函数的原型
6.深拷贝和浅拷贝【数据的类型】
基本类型:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)。
引用类型: 对象(Object)、数组(Array)、函数(Function)
内存机制:
栈内存(基本类型): 先进栈,后出栈 堆内存(引用类型):引用数据会在栈内存种开辟一个内存空间
JSON.parse(JSON.stringify())
- 无法实现深拷贝,无法拷贝 函数、正则、时间格式、原型上的属性和方法等
- 可以用递归实现深拷贝
案例1:不支持undefined、函数和循环引用的情况
const obj = {
name: "Tom",
age: 11,
height: undefined,
dsc() {
console.log(`${this.name} is ${this.age} years old!`)
}
}
const cloneObj = JSON.parse(JSON.stringify(obj))
console.log("cloneObj", cloneObj)
结果:
案例2:递归拷贝
const obj = {
name: "Tom",
age: 11,
height: undefined,
dsc() {
console.log(`${this.name} is ${this.age} years old!`)
}
}
function deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (cache.has(obj)) return cache.get(obj) // 如果出现循环引用,则返回缓存的对象,防止递归进入死循环
let cloneObj = new obj.constructor() // 使用对象所属的构造函数创建一个新对象
cache.set(obj, cloneObj) // 缓存对象,用于循环引用的情况
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], cache) // 递归拷贝
}
}
return cloneObj
}
obj.a = obj // 循环引用
const newObj = deepClone(obj)
console.log("newObj", newObj)
结果:
7.事件轮询机制 【Event Loop】也叫事件的执行机制
js语言是最大的特点就是
单线程,也就是说,同时只能做一件事情。所有的任务都需要排队,前一个任务结束,后一个任务才会执行。如果前一个任务耗时过长,后一个任务就不得不一直等待
js执行代码时,看当前执行的代码是同步任务还是异步任务,如果是
同步任务直接执行,当遇到异步任务,就会将琦放在异步队列中,当同步任务执行完毕,待会去执行异步任务
所有的任务分为两种:宏任务和微任务
graph TD
js任务 --> 同步任务
js任务 --> 异步任务 --> 宏任务
异步任务 --> 微任务
微任务(优先):promise、async/await、nextTick
宏任务:主代码块、dom事件回调、ajax回调、定时器回调
事件轮询机制执行过程
- 代码在执行过程中, 宏任务和微任务放在不同的队列中
- 宏任务执行完了,看是否有微任务,如果有就执行,没有直接执行下一个宏任务
- 微任务执行结束,会执行宏任务队列中排在第一个的宏任务,依次类推
async、await事件轮询执行时机
async隐式返回Promise,会产生一个微任务
await后面的代码是在微任务时执行
案例1:setTimeout和Promise
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
new Promise((resolve) => {
console.log("Promise"); // 这里是同步代码
resolve();
})
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
案例2:async/await
console.log("script start");
async function async1() {
await async2(); // await 隐式返回promise
console.log("async1 end"); // 这里的执行时机:在执行微任务时执行
}
async function async2() {
console.log("async2 end"); // 这里是同步代码
}
async1();
console.log("script end");
案例3:综合:
console.log("script start");
async function async1() {
await async2(); // await 隐式返回promise
console.log("async1 end"); // 这里的执行时机:在执行微任务时执行
}
async function async2() {
console.log("async2 end"); // 这里是同步代码
}
async1();
setTimeout(function() {
console.log("setTimeout");
}, 0);
new Promise(resolve => {
console.log("Promise"); // 这里是同步代码
resolve();
})
.then(function() {
console.log("promise1");
})
.then(function() {
console.log("promise2");
});
console.log("script end");
// 打印结果: script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
vue nextTick优先于微任务执行
Node中的process.nextTick执行顺序早于微任务
从event loop规范探究javaScript异步及浏览器更新渲染时机
Vue异步更新 - nextTick为什么要microtask优先
浏览器与Node的事件循环(Event Loop)有何区别?
8.Promise
Promise有两个特点:
- 对象的状态不受外界影响,有三种状态
pending(进行中)、fulfilled(已成功)、rejected(已失败) - 一旦状态改变,就不会再变
1.创建一个Promise
const p = new Promise((resolve, reject) => {//执行器函数
// 2.执行异步操作任务
setTimeout(() => {
const time = Date.now();
if (time % 2 == 0) {
resolve('success:'+time)
} else {
reject('fail:'+time)
}
}, 1000);
})
const p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve(1)},100)});
const p2 = Promise.resolve(2);
const p3 = Promise.reject(3);
链式调用
p.then(res=>console.log('成功=',res),err=>console.log('失败=',err))
2.Promise.all [返回promises列表中全部执行完的结果]
const pAll = Promise.all([p1,p2])
pAll.then(res=>console.log('pAll成功=',res),err=>console.log('pAll失败=',err))
3.Promise.race [返回promises列表中第一个执行完的结果]
const pRace = Promise.race([p1,p2])
pRace.then(res=>console.log('pRace成功=',res),err=>console.log('pRace失败=',err))
4.retry:当接口失败之后,每隔几秒,再重发一次
/*
* @param {function} fn - 方法名
* @param {number} delay - 延迟的时间
* @param {number} times - 重发的次数
*/
function retry(fn, delay, times) {
return new Promise((resolve, reject) => {
function func() {
Promise.resolve(fn()).then(res => {
resolve(res);
})
.catch(err => {
// 接口失败后,判断剩余次数不为0时,继续重发
if (times !== 0) {
setTimeout(func, delay);
times--;
} else {
reject(err);
}
});
}
func();
});
}
案例:try/catch的异步捕获
function something(){
if(Date.now() % 2 === 1){
console.log("当前是奇数,可以执行任务");
}else{
throw new Error('当前时间为偶数,无法执行')
}
}
// 捕获异常
try {
something()
} catch (err){
console.log(err.stack);
}
9.定时器
JS提供了一些原生方法来实现延时去执行某一段代码
1.setTimeout和setInterval【两者最短时长为4ms】
setTimeout 固定时长后执行 setInterval 间隔固定时间重复执行
console.log("先执行这里");
setTimeout(() => {
console.log("执行啦");
},0);
2.定时器不准的原因
setTimeout和setInterval都是宏任务,根据事件轮询机制,其他任务会阻塞或者延迟js任务的执行
3.setTimeout/setInterval 动画卡顿
不同设备的屏幕刷新频率可能不同,setTimeout/setInterval只能设置固定的时间间隔,设置的时间和屏幕的刷新时间可能不一致
setTimeout/setInterval通过设置一个间隔时间,来不断的改变图像实现动画效果,在不同的设备上可能出现卡顿、抖动等现象
4.requestAnimationFrame 是浏览器专门为动画提供的API
requestAnimationFrame 刷新频率与显示器的刷新频率保持一致,使用该api可以避免setTimeout/setInterval造成的动画卡顿情况
requestAnimationFrame告诉浏览器在下次重绘之前执行传入的回调函数(操纵dom,更新动画的函数)
setTimeout/setInterval与requestAnimationFrame的区别?
用requestAnimationFrame代替setInterval定时器
优点:
1.requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘
2.充分利用显示器的刷机机制,节省系统系统资源。
4.requestAnimationFrame启动动画,默认返回一个id,cancelAnimationFrame只需要传入这个id就可以停止了。
大批量数据优化
案例1:进度条
<div style="width: 0px; height: 12px; line-height: 12px; background: pink">
0%
</div>
<script>
var div = document.querySelector("div");
div.onclick = function () {
var timer = requestAnimationFrame(function fn() {
if (parseInt(div.style.width) < 300) {
div.style.width = parseInt(div.style.width) + 3 + "px";
div.innerHTML = parseInt(div.style.width) / 3 + "%";
timer = requestAnimationFrame(fn);
} else {
cancelAnimationFrame(timer);
}
});
};
// requestAnimationFrame如何停止动画:cancelAnimationFrame()只接受一个参数,
// requestAnimationFrame 默认返回一个id,cancelAnimationFrame只需要传入这个id就可以停止了。
// requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
// 1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
// 2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
</script>
案例2:直线运动
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<style>
#e {
width: 100px;
height: 100px;
background: red;
position: absolute;
left: 0;
top: 0;
zoom: 1;
}
</style>
</head>
<body>
<div id="e"></div>
<script>
var e = document.getElementById("e");
var flag = true;
var left = 0;
function render() {
if (flag == true) {
if (left >= 500) {
flag = false;
}
e.style.left = ` ${left++}px`;
} else {
if (left <= 0) {
flag = true;
}
e.style.left = ` ${left--}px`;
}
}
// 方法1
// setInterval(function () {
// render();
// }, 1000 / 60);//1000/60,这是因为大多数屏幕渲染的时间间隔是每秒60帧
// 方法2
// requestAnimationFrame效果
// (function animloop() {
// render();
// window.requestAnimationFrame(animloop);
// })();
// requestAnimationFrame如何停止动画:cancelAnimationFrame()只接受一个参数,
// requestAnimationFrame 默认返回一个id,cancelAnimationFrame只需要传入这个id就可以停止了。
// 方法2,让动画停止
(function animloop(time) {
// console.log(time,Date.now())
render();
rafId = requestAnimationFrame(animloop);
//如果left等于50 停止动画
// if (left == 300) {
// cancelAnimationFrame(rafId);
// }
})();
// requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
// 1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
// 2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
</script>
</body>
</html>
10 get和post的区别
误区:
我们经常说get请求参数的大小存在限制,而post请求的参数大小是无限制的。
- http协议未规定get和post的长度限制
- get的最大长度显示是因为浏览器和web服务器限制了URL的长度
- 不同的浏览器和web服务器,限制最大长度不一样
区别:
get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,可以使用缓存 post一般是修改和删除,必须与数据库交互,不能使用缓存
11 事件流
html和javascript交互是通过事件驱动来实现的,比如:onClick等。
事件流:事件流描述的是从页面种接收事件的顺序
事件捕获阶段 事件的目标阶段 事件的冒泡阶段