JS
- JS
JS
var const let 以及作用域等概念
作用域
- js本来只有两种作用域:全局作用域和函数作用域;es6引入了let\const和块级作用域。
全局作用域
不用说了,就是在最外层声明的变量。
javascript
var a=0;
函数作用域
javascript
function fun(){
var a=0;
}
块级作用域:函数作用域 if while for {} 等;
可以看到,函数作用域也是块级作用域。块级作用域简单地理解,就是找花括号{}。
- 块级作用域 vs 函数作用域
- var的作用域是函数作用域和全局作用域;let和const的作用域是块级作用域;
- 下面代码中的var a=0;你在fun中的任何一个块级都能访问和更改。改成let a=0;也是能在任何一个块级访问和更改。
javascript
function fun() { // 块级1:函数块
var a = 0;
if (true) {} // 块级2: 条件块
while (true) {} // 块级3:循环块
function inner() {} // 块级4:函数块
for (let i = 0; i < 5; i++) {} // 块级5:循环块
}
为了简单,简化成下面的代码
javascript
function fun() { // 块级1:函数块
var a = 0; // // 改成let a=0,块级2也能正常访问
if (true) { // 块级2: 条件块
a=2;
}
function inner() {} // 块级4:函数块
console.log(a); // 2
}
fun();
- var a=0,这句话是块级1的,块级1是函数块。从函数作用域的角度来说,var的作用域是整个函数,就是fun。假设改成let a=0,那么let的作用域是块级作用域,也就是块级1。块级2包含在块级1中,所以能更改a的值。
为什么要有let? 防止变量作用域上升。
javascript
function fun() { // 块级1:函数块
var a = 0;
if (true) { // 块级2: 条件块
var a=2;
}
function inner() {} // 块级4:函数块
console.log(a); // 2
}
fun();
假设代码改成这样,在块级2中重新声明a,会发现,最终输出的a还是2。这就造成了变量污染。因为在块级2中,他是一个条件块,不是一个函数块,所以在块级2中声明的变量会上升,其实也就是会污染离它最近的一个函数块的变量。
上面例子和下面例子做对比,注意体会var的作用是函数作用域这句话!下面的var a=0,写在了函数块,所以才没有影响fun中的a;上面的a写在了条件块,条件块不会函数块,所以条件块里面的变量的作用域是条件快所在的函数块。
javascript
function fun() { // 块级1:函数块
var a = 0;
if (true) { // 块级2: 条件块
}
function inner() {
var a=2;
} // 块级4:函数块
console.log(a); // 0
}
fun();
显然,在前面例子中,我们在条件代码块中用var重新声明a,会影响外面的的值,形成了变量作用域"上升"的感觉。所以,就诞生了let,let就是为了防止这种上升。
javascript
function fun() { // 块级1:函数块
let a = 0;
if (true) { // 块级2: 条件块
let a=2; // 重新声明不会影响外面的a,这个作用域只会在当前块级,不区分当前块级是否是函数块
}
function inner() { // 块级4:函数块
}
console.log(a); // 0
}
fun();
例题
javascript
for (var i = 0; i < 5; i++) {
// 输出5个5
setTimeout(()=>console.log(i),1000)
}
改成let
javascript
for (let i = 0; i < 5; i++) {
setTimeout(()=>console.log(i),1000)
}
for循环的底层大概逻辑
javascript
for (var i = 0; i < 2; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
会执行2次代码块 ,每个代码块会重新声明var i=0;代码了执行完后,i++,i完成自增后,假设为1,复制给下一次var i=1;
所以:实际差不多是这样子:
javascript
{
let tmp=0; // 初始值
{
var i=tmp;
if(i<2){
setTimeout(()=>console.log(i));
tmp++;
}
}
{
var i=tmp;
if(i<2){
setTimeout(()=>console.log(i));
tmp++;
}
}
}
javascript
for (let i = 0; i < 2; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
javascript
{
let tmp=0; // 初始值
{
let i=tmp;
if(i<2){
setTimeout(()=>console.log(i));
tmp++;
}
}
{
let i=tmp;
if(i<2){
setTimeout(()=>console.log(i));
tmp++;
}
}
}
原型
原型对象
- 可以理解成一个模板,没有官方考证嗷,但是感觉应该是只有function才有原型,箭头函数都没有。
- js中可以根据这个模板来实例化对象。好处就是,模板可以共享。原型自己本身也是一个对象。
javascript
function Father(){};
console.log(Father.prototype)
输出结果:
- 原型中有两个属性:
- constructor:指向构造函数本身Father。 Father.prototype.constructor==Father // true
- poro : 指向原型对象的原型对象。也就是,Father.prototype.proto==Object.prototype // true
- 可以看到函数Father的原型是 {constructor:f,proto :Object} as P, 那么P的原型就是Object.prototype。这也说明了,原型本身是一个对象。anyway:原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null (原型链查找)
prototype vs proto
- 都指向原型对象。prototype针对函数而言。__proto__针对实例对象而言。
- A的__proto__指向构造它的对象的原型。也就是假设A是根据构造函数F()来的,那么A.__proto__指向F.prototype
- A.prototype是undefined,因为prototype是针对函数(function)而言的
基于原型链的继承
一个对象,会继承其原型的属性和方法。查找一个对象的属性,js会在当前对象找,找不到,就去找原型,原型的原型...(本质就是迭代__proto__)这就是原型链查找。
javascript
function Father(){
this.familyName="sam"
};
Father.prototype.sex="y" // y染色体
let son=new Father();
console.log(son,son.sex)
可以看到,son是根据Father函数new出来的,单独打印son,只有familyName这个属性,但是你访问son.y也有值,原因就是js在son中没找到y,就去son.__proto__里面找,而son.proto =Father.prototype。举一反三,Father.prototype.sex那句话改成Object.prototype也是一样的效果。你也可以通过son.proto.sex进行修改,注意,son.sex只会影响son本身。
this 关键字
- this是js中的关键字;动态的时候确定;
- call apply bind 就是改变运行时候this的指向。比如,fun.call(obj);其实就是运行这句话的时候,调用fun,并把this指向obj。
- 构造函数的原理,也就是new的原理,其实就是利用了这一点实现的。
- new constructorFun()发生了什么?
- 创建一个新对象obj;
- this绑定成obj,并运行构造函数constructorFun;从而达到通过constructorFun的逻辑改变obj的属性和值。(这也就是为什么constructorFun里面往往都是this.xxx=yyy;
手写new函数
手写防抖和节流
防抖
基本函数
javascript
// 初级防抖函数
function debounce(fn,delay){
let timer;
return function(...args){
clearTimeout(timer);
timer=setTimeout(()=>fn.apply(this,args),delay);
}
}
// 自定义
let d=debounce(()=>console.log("excute fu"),100);
// 抖动5次,只会执行最后一次
for(let i=0;i<5;i++){
d();
}
// 输出
// excute fu
防抖是什么?防止抖动。如果一直抖动,只执行最后一次。
闭包的应用
- 什么是闭包?函数+函数访问的变量。特点是可以访问函数外面的变量;其实debounce本质上返回的是一个函数定义,也就是下面的d函数,显然,d函数访问了d函数外的timer,那么d函数和timer构成了闭包。这也是实现防抖的关键。
- timer变量,只要有函数引用,就不会消失。五次抖动中(循环),每次循环,都返回了debounce的d函数,我们可以对应成d1,d2,d3,d4,d5;实际上di都访问了timer,然后,di+1会清除掉di创建的timer,并且将timer指向新的timer,其实也就是覆盖。由此,只有最后一次会生效。
javascript
function debounce(fn,delay){
let timer;
let d=function(...args){
clearTimeout(timer);
timer=setTimeout(()=>fn.apply(this,args),delay);
}
return d;
}
进阶函数
- 怎么实现立刻执行?上面的实现效果是,n次抖动中,只执行第n次,并且,是在第n次发生delay时间后执行fn函数。那么,如果希望n次抖动中,执行且仅执行第1次呢?也就是所谓的立马执行。
javascript
// 进阶
function debounce(fn,delay,immedate){
let timer;
let d=function(...args){
clearTimeout(timer);
if(immedate){
let callNow=!timer; // callNow只有第一次会为真
timer=setTimeout(()=>timer=null,delay);// delay有没有都无所谓,不影响,这里只是说最后一次抖动完之后,delay时间后,再彻底清除掉timer。
if(callNow){
fn.apply(this,args);
}
}else{
timer=setTimeout(()=>fn.apply(this,args),delay);
}
}
return d;
}
let d=debounce(()=>console.log("excute fu"),100,true);
for(let i=0;i<5;i++){
d();
}
- callNow控制第一次执行。上述逻辑,只有d1的时候,callNow是真,因为timer初始状态为null;d2,d3,d4,d5的时候timer都不会为null;因为
timer=setTimeout(()=>timer=null,delay);// delay有没有都无所谓,不影响,这里只是说最后一次抖动完之后,delay时间后,再彻底清除掉timer。
等到最后一次,也就是d5执行了,隔delay后,清除timer;
节流基本函数
节流定义
节流就是在一定时间内只执行一次。
防抖:执行最后一次 vs 节流:每隔一定时间内执行且只执行一次
javascript
// 基础
function throttle(fn,await){
let timer;
return function (...args) {
if(!timer){
timer=setTimeout(()=>{
fn.apply(this,args);
timer=null; // 不能用clearTimeout清除定时器;因为只是暂停了,打印timer还是会有值,那么if(!timer)等于没用了
},await);
}
}
}
- 有timer就略过,没有timer就指定新的定时器;唯一需要注意的可能就是timer=null,而不是clearTimeout(timer);当然,为了防止内存泄露,也是可以加上句话的。
进阶函数
- 节流一般问的可能就是,定时器不一定准确,那么你怎么控制?
- 首先,定时器为什么不准确。
- 浏览器本身因素,设备熄屏计时器可能会暂停啊什么的
- 事件循环机制
- setTimeout属于宏任务。计时器结束之后的函数会进入宏任务队列。宏任务队列之前可能有其他函数。
由此可见,首先是你定时器本来就不准,其次是就算准确了,进入队列,也有可能需要等其他任务执行完了,才能执行定时器的回调函数。那么,我们至少可以控制第一个因素,定时器本身就不准确------时间戳解决。
javascript
function throttle2(fn,awaitt) {
let timer;
let start=Date.now();
return function (...args) {
let now=Date.now();
let remain=awaitt-(now-start);
clearTimeout(timer); // 如果有程序一直调的话,需要清除上一次产生的定时器
if(remain<=0){ // 到时间了,直接执行;
fn.apply(this,args);
start=Date.now(); // 重置;
}
else{ // 如果函数只被触发了一次
timer=setTimeout(fn,remain); // 还剩remian时间到达指定的awaitt时间长度;
}
}
}
事件循环
不一定对,可能是必要不充分推论。
执行顺序
- 同步代码;
- 微任务(如果有)
- 取宏任务
- 清空微任务
- 渲染 (一次循环结束)
- 下一个宏任务 (下一次循环)
异步代码会产生两种任务:
- 常见微任务代码:promise.then
- 宏任务:setTimeout
async/await
有点特殊
线程 vs 任务队列 vs await详解
线程 vs 任务队列
- js是单线程的;但是js的宿主环境:浏览器/node.js是多线程的。比如遇到Promise.then, async的await标记的函数的后面的代码块,setTimeout中的代码块,宿主环境都会开辟新的线程去执行,等到有结果返回的时候,再回调到任务队列中(微任务和宏任务队列)。js再通过事件循环来执行。
- Promise.then(fun),浏览器会等到Promise有状态的时候(resolve/reject)之后,也就是then/catch满足条件之后,把fun塞到微任务队列中;
await详解
async fun(){ await fun1(); code2}
;遇到await会暂停原来的线程,立刻执行await标记的函数,也就是fun1(),等到fun1()执行完了之后,也就是await收到了一个状态已经resolve的Promise对象 ,那么就会任务满足条件了,就会把await的下一行,也就是剩下的代码块code2放进微任务队列之中。- await后面跟着的是一个promise对象,只会接收到该对象的resolve结果 ,reject结果是接收不到的,需要try-catch来捕捉;所以,如果await fun1()中,fun1()是Promise.reject,那么code2是不会进入微任务的。
- await fun;如果fun自己返回的就是一个promise对象,很好理解,如果不是,比如fun返回的是基本数据类型,或者没有返回值,那么await会自动将其封装成Primise.resolve(fun)/**Primise.resolve(undefined),**然后,await再去拿Promise.resolve的值;
- 遇到setTimeout(fun,await),浏览器会开辟一个线程挂起,等到await事件后,讲fun塞到宏任务队列;