面经 | 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塞到宏任务队列;
相关推荐
狸克先生2 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
尘浮生4 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
sinat_384241094 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
baiduopenmap16 分钟前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
hopetomorrow18 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
loooseFish24 分钟前
小程序webview我爱死你了 小程序webview和H5通讯
前端
小牛itbull28 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i36 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_39 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
闲暇部落39 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin