面经 | 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塞到宏任务队列;
相关推荐
DARLING Zero two♡26 分钟前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技
Gu Gu Study28 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
芊寻(嵌入式)1 小时前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
有梦想的咸鱼_1 小时前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
海阔天空_20131 小时前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
天下皆白_唯我独黑1 小时前
php 使用qrcode制作二维码图片
开发语言·php
夜雨翦春韭1 小时前
Java中的动态代理
java·开发语言·aop·动态代理