前端知识补充

听的内容是B站里面的课程——javascript李立超,之前直接看的javascript高级(第四版)虽然看的还是比较仔细了,但是有些内容可能直接看书有没有理解的地方,现在听下课会理解的更加深刻。

数组去重

// 数组去重
let arr = [2,4,5,3,4,3,5,2];
// 思路一:新建数组,将原数组的值传递到新数组 如果新数组中已经有了则不传了
const newArr = [];
for(let i of arr) {
    if(newArr.indexOf(i) === -1) {
        newArr.push(i);
    }
}
// 思路二:如果不使用多余的空间 取出每一个数,跟后面的进行比较 这样其实时间复杂度为O(N2)
for(let i = 0;i<arr.length-1;i++) &#123;
    for(let j = i+1;j<arr.length;j++) &#123;
        if(arr[i] === arr[j]) &#123;
            arr.splice(j,1);
            /*
                当arr[i]和arr[j]相同时,它会自动的删除j位置的元素,然后与j+1位置的元素进行比较,但是删除之后j+1的位置就移动到了j位置,而j位置不会再进行比较,就会出现漏比较的情况          
                解决办法:当删除一个元素后,需要将该位置的元素提前一个
            */
            j--;
        &#125;
    &#125;
&#125;
// 思路三:对每一个元素进行检索 看数组后面是否还有该数 如果有的话就进行删除
for(let i = 0;i<arr.length;i++) &#123;
    const index = arr.indexOf(arr[i],i+1);
    if(index !== -1) &#123;
        arr.splice(index,1);
        // 这里i--与上面的j--是一样的道理
        i--;
    &#125;
&#125;

定时器

定时器的本质就是在指定时间后将函数添加到消息队列中

// 添加一个计时器
console.time()
setTimeout(function () &#123;
    console.timeEnd()
    console.log('定时器执行了!');
&#125;,3000)

// 使程序卡6s
const begin = Date.now()
while(Date.now() - begin < 6000) &#123;&#125;

// 这里计时器会显示6s 因为虽然定时器将代码3s后添加到了消息队列中,但是此时执行栈中依旧在执行6s的那个程序,需要将6s运行完成之后,从消息队列中取出定时器的代码执行。

setInterval()每间隔一段时间就将函数添加到消息队列中,但是如果函数执行的速度比较慢,它是无法确保每次执行的间隔都是一样的

console.time('interval:');
setInterval(function () &#123;
    console.timeEnd('interval:');
    alert('executor!!');
    console.time('interval:');
&#125;,3000);

// 这里会随着alert点击的速度导致每次代码中的间隔执行时间不同 
// 如果点击快  则不受影响 如果点击慢(长于3s)则定时器已经将代码提交到消息队列了,所以一点点击之后,直接就执行

如果想要可以确保每次执行都有相同的间隔,可以使用setTimeout

console.time('interval:');
setTimeout(function fn() &#123;
    console.timeEnd('interval:');
    alert('executor!!');
    console.time('interval:');
    // 在setTimeout的回调函数最后,再调用一个setTimeout
    setTimeout(fn, 3000);
&#125;, 3000)
// 这样每次都是3s后执行,不管点击的速度是多少,因为点击的时间并没有在定时器中,而是点击之后才开启的定时器

解构

对象解构

// 写法
const obj = &#123; name: 'sunwukon', age: 18, gender: 'male'&#125;;
let &#123;name, age, gender&#125; = obj; // 声明变量的同时解构对象
let name, age, gender;
(&#123;name, age, gender&#125;) = obj; // 声明时未赋值 需要在前面添加一个() 因为&#123;&#125;开头解析器会将其解析为代码块,因此会报错
// a b c是别名,可以默认给值赋值为花果山
let &#123;name: a, age:b, gender:c, address:d = 'huaguoshan'&#125; = obj

数组解构

const arr = ['sunwukong', 'zhubajie','shaheshang'];
let [a, b, c] = arr;
let [d,e,f,g] = arr;  // 这里会给g赋值为undefined
// 声明之后还可以赋值  可以给f和g添加默认值
[d,e,f = 77,g = 'yutujing'] = arr;
// 可以使用...来设置获取多余的元素
// 这里n3就是一个数组 [6,7]
let [n1,n2,...n3] = [4,5,6,7];

可以通过解构赋值来快速交换两个变量的值

let a1 = 10;
let a2 = 20;
// 前面的[]中的数指的是数组解构的形式 后面[]中的数是变量,整个[]就是一个数组,所以就是通过后面的数组给前面的数组变量赋值
[a1,a2] = [a2,a1];

闭包

闭包主要用来隐藏一些不希望被外部访问的内容,这就意味着闭包需要占用一定的内存空间

相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能),每创建一个闭包,闭包中的不希望被外部访问的那些元素就会被创建一次

解决:需要执行次数较少时,使用闭包,需要大量创建实例时,使用类

定义

闭包就是能够访问到外部函数作用域中变量的函数

使用

当我们需要隐藏一些不希望被被人访问的内容时就可以使用闭包

构成条件

  1. 函数的嵌套
  2. 内部函数要引用外部函数中的变量
  3. 内部函数要作为返回值返回
function fn3() &#123;
    let a = "fn3's a";
    function fn4() &#123;
        console.log(a)
    &#125;
    fn4()
&#125;
fn3() // 这里打印的是fn3中的a

这里涉及到一个非常重要的知识点:函数的作用域,在函数创建时就已经确定了(词法作用域),和它调用的位置无关。

上述例子中的fn4的作用域在fn3中创建的,因此它打印a时会从自己的作用域中查找,没有,就到上级作用域中找,它的上级作用域就是fn3,因此它会打印出fn3的a。

function fn3() &#123;
    let a = "fn3's a";
    function fn4() &#123;
        console.log(a)
    &#125;
    return fn4()
&#125;
let fn4 = fn3() 
fn4(); // 这里打印的还是fn3中的a 原因依旧如此 fn4只要是在fn3中创建的且他自己本身没有a 那么它的a就会在fn3中找,跟它调用的位置没有关系
// 再来一个简单的例子
let a = 'window a';
function fn() &#123;
    console.log(a)
&#125;
function fn2 ()&#123;
    let a = 'fn2 a';
    fn()
&#125;
fn2(); // 这里打印出来的a是window a 因为fn是在全局中创建的

闭包的生命周期

  1. 闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
  2. 在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)
function outer2() &#123;
    let num = 0;
    return () => &#123;
        num++;
        console.log(num);
    &#125;
&#125;
let fn1 = outer2(); // 独立闭包
let fn2 = outer2(); // 独立闭包
// 每个闭包里面的num都是独立的,不会随着一个闭包的调用而相互影响
fn1() 
fn2()
// 销毁闭包 将其外部函数指向null  垃圾回收机制就会自动回收闭包中的内容
fn1 = null
fn2 = null

高阶函数

如果一个函数的参数或者返回值是函数,则这个函数就称为高阶函数

为什么要将函数作为参数传递?(回调函数有什么作用?)

  • 将函数作为参数,意味着可以对另一个函数动态的传递代码
// 这里的filter就是一个高阶函数
function filter(arr,cb) &#123;
    const newArr = []
    
    for(let i = 0;i<arr.length;i++) &#123;
        if(cb(arr[i]))
            newArr.push(arr[i])
    &#125;
    return newArr;
&#125;
const arr = [1,2,3,4,5,6,7,8,9,10];
// 通常后面的函数都是以匿名函数或者箭头函数定义的
result = filter(arr,a=>a % 2 === 0)
console.log(result);

浅拷贝与深拷贝

说明一下数组中有引用数据类型的内存占用情况

const arr = [&#123;name:'孙悟空'&#125;,&#123;name:'猪八戒'&#125;];

浅拷贝(shallow copy)

  • 通常对对象的拷贝都是浅拷贝
  • 浅拷贝只对对象的浅层进行复制(只复制一层)
  • 如果对象中存储的数据是原始值,那么拷贝的深浅是不重要
  • 浅拷贝只会对对象本身进行复制,不会复制对象中的属性(或者元素)

深拷贝(deep copy)

  • 深拷贝指不仅复制对象本身,还复制对象中的属性和元素
  • 因为性能问题,通常情况都不太使用深拷贝

数组浅拷贝

// 如果splice里面没有参数就说明全部拷贝
const arr2 = arr.splice()

对arr和arr2进行一些操作,说明他们是浅拷贝

arr === arr2   // false
arr[0] === arr2[0] // true
arr[0].name = '唐僧'
arr2[0].name // '唐僧' 因为arr[0]和arr2[0]都指向相同的位置

对象浅复制

Object.assign(目标对象,被复制的对象):将被复制对象中的属性赋值到目标对象里,并将目标对象返回

const obj = &#123;name: '孙悟空', age:18&#125;
const obj2 = &#123;address: '花果山', age: 28&#125;
Object.assign(obj2,obj) 
console.log(obj2) // name: '孙悟空', age: 28, address: '花果山'

…(展开运算符) 实现浅拷贝

  • 可以将一个数组中的元素展开到另一个数组中或者作为函数的参数传递
// 也可以使用展开运算符对对象进行复制 将obj中的属性在新对象中展开
const obj3 = &#123;address: '花果山', ...obj, age: 48&#125;

数组深拷贝

const arr = [&#123;name:'孙悟空'&#125;,&#123;name:'猪八戒'&#125;];
const arr3 = structuredClone(arr) // 专门用来深拷贝的方法

对象深拷贝

同样还是使用structuredClone方法

this指向

根据函数调用方式不同,this的值也不同:

  • 以函数形式调用,this是window
  • 以方法形式调用,this是调用方法的对象
  • 构造函数中,this是新建的对象
  • 箭头函数没有自己的this,由外层作用域决定
  • 通过call和apply调用的函数,它们的第一个参数就是函数的this
  • 通过bind返回的函数,this由bind第一个参数决定(无法修改)

bind()是函数的方法,可以用来创建一个新的函数

  • bind可以为新函数绑定this
  • bind可以为新函数绑定参数

箭头函数没有自身的this,它的this由外层作用域决定,也无法通过call apply和bind修改它的this,箭头函数的this和它的调用方式无关,箭头函数中也没有arguments

var name = 'ba'
let obj = &#123;
    name: 'sun',
    age: 18,
    sayName() &#123;
        console.log("name:" + name);
    &#125;
&#125;
obj.sayName(); // ba 因为name是静态的,写死的,它会找函数创建的上级作用中的name 就是全局作用域中的name

let obj2 = &#123;
    name: 'sun',
    age: 18,
    sayName() &#123;
        console.log("name:" + this.name);
    &#125;
&#125;
obj2.sayName(); // sun 因为name是this上面的,是动态的,会随着this的指向变化 this现在指向的就是obj2

箭头函数

定义

([参数]) => 返回值

例子

无箭头函数:() => 返回值

一个参数的:a => 返回值

多个参数的:(a,b) => 返回值

只有一个语句的函数:() => 返回值

只返回一个对象的函数:() => ({…}) 对象需要用()包裹

有多行语句的函数:() => { … return 返回值}

立即执行函数

IIFE

立即是一个匿名的函数,它只会调用一次

可以利用IIFE来创建一个一次性的函数作用域,避免变量冲突的问题

(function() &#123;
    let a = 10;
    console.log(111)
&#125;());
// 因为都有a 避免冲突 放到函数作用域中
(function() &#123;
    let a = 20;
    console.log(222)
&#125;())

题目练习

涉及到变量提升 函数提升 this指向

var a = 1
function fn() &#123;
    console.log(a) // undefined 因为变量提升 但此时a还并没有赋值
    var a = 2
    console.log(a) // 2 此时a已经赋值
&#125;
fn()
console.log(a) // 1 打印的全局的a
console.log(a) // 打印出function a()&#123;...&#125; 因为第一个变量a和函数都会提升,但是函数还会进行赋值 也可以理解为函数提升的优先级更高
var a = 1
console.log(a) // 1 给a重新赋值了
function a() &#123;
    alert(2)	
&#125;
console.log(a) // 1 因为函数已经提升且赋值 相当于上述代码早已经执行了
var a = 3
console.log(a) // 3 a重新赋值
var a = function() &#123;
    alert(4)
&#125;
console.log(a) // function()&#123;...&#125; 给a重新赋值 
var a
console.log(a) // function()&#123;...&#125; 因为a早已经通过第一个a声明提升了,后面的都不能进行声明
// 函数每次调用 都会重新创建默认值 就是说如果使用的是默认值,则每次调用都是不同的变量(内存位置不同),就算在函数内部修改了对其他的该函数调用并无影响
function fn2(a = &#123;name: "沙和尚"&#125;) &#123;
    console.log("a = " + a)
    a.name = "唐僧"
    console.log("a = " + a)
&#125;
fn2() // 沙和尚 唐僧
fn2() // 沙和尚 唐僧

let obj = &#123;name: "沙和尚"&#125;
// 如果是外部传值,则每次调用都是同一个对象,修改之后对之后重新调用函数有影响
function fn2(a = obj) &#123;
    console.log("a = " + a)
    a.name = "唐僧"
    console.log("a = " + a)
&#125;
fn2() // 沙和尚 唐僧
fn2() // 唐僧 唐僧
var a = 1;
function fn() &#123;
    a = 2; // 这里的a是全局的a
    console.log(a) // 2
&#125;
fn()
console.log(a) // 2
var a = 1
// 这里fn接收一个形参,这个形参就是局部作用域中的变量,而不是全局的。
// 形参相当于声明了该变量,如果调用函数时没有赋值,则说明应该是undefined 否则就是传入的值
function fn(a) &#123;
    console.log(a) // 1
    a = 2;
    console.log(a) // 2
&#125;
fn(a)
console.log(a) // 1

同步与异步

接下来这个题目考察到了作用域的问题以及同步异步执行以及立即执行函数

for(var i = 1;i<5;i++) &#123;
    console.log(i);
&#125;  // 1 2 3 4 
for(var i = 1;i<5;i++) &#123;
      setTimeout(() => &#123;
        console.log(i) // 5 5 5 5
      &#125;,0)
    &#125;
// 这里的i是通过var声明的,相当于是全局作用域中的i 
// 上述代码可以改写成
    var i=1;
    setTimeout( function timer()&#123;
        console.log( i ); 
    &#125;, i*1000 );

    var i=2;
    setTimeout( function timer()&#123;
        console.log( i ); 
    &#125;, i*1000 );

    var i=3;
    setTimeout( function timer()&#123;
        console.log( i ); 
    &#125;, i*1000 );

    var i=4;
    setTimeout( function timer()&#123;
        console.log( i ); 
    &#125;, i*1000 );

    var i=5;//i<=5 结束循环
    
    //输出 5 5 5 5
    // 会先执行同步代码 那么后面的赋值会覆盖掉前面的赋值 最后i = 5
// 然后再执行异步代码setTimeout 但是此时打印出来的i都是5了
// 使用let 它有块级作用域,所以每次循环的i都只在当前循环有效 后面i不会覆盖掉前面的i
for(let i = 1;i<5;i++) &#123;
      setTimeout(() => &#123;
        console.log(i) // 1 2 3 4
      &#125;,0)
    &#125;
// 这里虽然还是var 但是放到立即函数里面 把i传递到了函数中,就是函数中的局部变量了,不再是全局变量,因此里面的异步打印出来的就是局部中的i的值
for(var i = 1;i<5;i++) &#123;
      (function (i) &#123;
        setTimeout(() => &#123;
          console.log(i) // 1 2 3 4
        &#125;,0)
      &#125;)(i)
    &#125;

其实js是单线程的,每个线程有它自己的唯一的事件循环,但是事件循环的任务源可以不唯一。类似setTimeout, promise, ajax, DOM操作等都是典型的任务源,任务队列中的任务便是来自这些任务源。而这些任务源产生的任务又可以分为macro-task(宏任务)和micro-task(微任务)两种。

macro-task(宏任务)

macro-task(宏任务)中的任务都是有时间顺序的,因此浏览器能够有序地从中调度任务并执行。在任务与任务之间,浏览器可能会渲染更新。
macro-task(宏任务)中一个典型就是setTimeout,setTimeout函数等待给定的延迟事件然后将其回调函数推入宏任务Event Queue中。这就是为什么先输出’script end’ 后输出’setTimeout’的原因。
macro-task(宏任务)主要有:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

micro-task(微任务)

micro-task(微任务)中的任务在当前函数调用栈中的函数执行完成之后即调度,像promise、mutation都会被推入微任务Event Queue队列中。并且微任务Event Queue队列中的一个任务执行完成后,后续的micro-task(微任务)也会继续执行,直到微任务Event Queue队列为空,这就解释了为什么promise2也会在setTimeout之前输出的原因。
微任务Event Queue队列主要有process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

当宏任务Event Queue队列中的一个任务执行结束时,如果函数调用栈为空,便会开始执行微任务Event Queue队列中的任务,直至微任务Event Queue队列中所有任务执行完毕,然后event loop才会继续执行宏任务Event Queue队列中的下一个任务。

宏任务它是通过宿主环境进行解析的(nodejs或者浏览器)微任务是js自己