前端面试——JS进阶

2022-07-25,,

JS进阶

1. ES6 基础知识点

变量提升

问:什么是变量提升?为什么存在变量提升

  • 函数及变量的声明都将被提升到函数的最顶部。也就是变量可以先使用再声明。
  • 变量提升的根本原因就是为了解决函数之间互相调用的情况。

问:变量和函数怎么进行提升的? 优先级是怎么样的?

  • 第一阶段:对所有的函数声明进行提升(忽略表达式和箭头函数),引用类型的赋值分为三步:
    • 开辟堆空间
    • 存储内容
    • 将地址赋值给变量
  • 第二阶段:对所有的变量进行提升,全部赋值为undefined,然后依次顺序执行代码(let和const时,不能在声明之前使用变量,这叫做暂时性死区)

var、let、const

问:var、let、const 三者的区别是什么

  • var 存在变量提升,而let、const则不会
  • var 在浏览器环境下声明的变量会挂载到window上,而其他两者不会。
  • let 和 const 的作用基本一致,后者声明的变量不能再次赋值(但是能改变值)

2. map、filter、reduce、foreach区别

map()

map(function(element,index,arr), thisValue)

传入一个函数,该函数会遍历数组,对每一个元素做变换之后,返回一个新数组

  • element: 对应数组的每个元素
  • index: 数组元素的下标
  • arr: 原数组
  • 可选。用作 “this” 的值。如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
  let arr = [2, 3, 4]
  let arr1 = arr.map(function (element, index, arr) {
    return arr[index] + 1
  }) 
  let arr2 = arr.map(function (element, index, arr) {
    return element + 1
  }) 
  console.log(arr); //  [2, 3, 4]
  console.log(arr1); // [3, 4, 5]
  console.log(arr2); // [3, 4, 5]

filter()

filter(function(currentValue,index,arr), thisValue)

传入一个函数,函数返回值为布尔类型,将返回值为真的元素放入新数组,返回这个新数组

  • element: 对应数组的每个元素
  • index: 数组元素的下标
  • arr: 原数组
  • thisValue: 可选。用作 “this” 的值。如果省略了 thisValue ,“this” 的值为 “undefined”
let arr = [1, 2, 3, 4]
let arr1 = arr.filter(function(element) {
  return element < 3
})
console.log(arr); //  [1, 2, 3, 4]
console.log(arr1); // [1, 2]

reduce()

reduce(function(total, element, index, arr), initialValue)

传入一个函数,返回一个值

  • total: 累计值(第一次的值代表初始化的值)

  • element: 对应数组的每个元素

  • index: 数组元素的下标

  • arr: 原数组

  • initialValue: 可选。传递给函数的初始值

let arr = [1, 2, 3]
let sum = arr.reduce(function(acc, element) {
  return acc + element
}, 1)
console.log(arr); // [1, 2, 3]
console.log(sum); // 7

forEach()

forEach(function(element, index, arr), thisValue)

  • element: 对应数组的每个元素
  • index: 数组元素的下标
  • arr: 原数组
  • initialValue: 可选。如果省略了 thisValue ,“this” 的值为 “undefined”

传入一个函数,直接操作原数组 没有返回值

let arr = [1, 3, 5]
let arr1 = arr.forEach(function(element, index, arr) {
  arr[index] = element+1
})
console.log(arr); // [2, 4, 6]
console.log(arr1); // undefined

3. 箭头函数

箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:

  • 函数体内的this, 指向定义时所在的对象,而不是使用时所在的对象
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数
  • 不可以使用new命令,因为它没有自己的this和prototype属性

4. this

this指向

  • 对象调用,this指向该对象(谁调用this就指向谁)

    var obj = { 
        name:'小鹿', 
        age: '21', 
        print: function(){ 
            console.log(this) 
            console.log(this.name + ':' + this.age) 
        } 
    }// 通过对象的方式调用函数 obj.print(); // this 指向 obj
    
  • 直接调用的函数,this指向的是全局window对象

    function print(){ 
        console.log(this); 
    }// 全局调用函数 
    print(); // this 指向 window
    
  • 通过new的方式,this永远指向新创建的对象

    function Person(name, age){ 
        this.name = name; 
        this.age = age; 
        console.log(this); 
    }
    var xiaolu = new Person('小鹿',22); // this = > xaiolu
    
  • 箭头函数中的this

    由于箭头函数没有单独的 this 值。箭头函数的 this 与声明所在的上下文相同。也就是说调用箭头函数的时候,不会隐式的调用 this 参数,而是从定义时的函数继承上下文。

    const obj = { 
        a:()=>{ 
            console.log(this); 
        } 
    }
    // 对象调用箭头函数 
    obj.a(); // window
    

如何改变this指向

我们可以通过调用函数的call、apply、bind来改变this的指向

var obj ={
    name:'zhangsan',
    age:18
}
function print() {
    console.log(this); //打印this的指向
    console.log(arguments); // 打印传递的参数
}
// 通过call 改变this指向
print.call(obj,1,2,3);

// 通过 apply 改变this 指向
print.apply(obj,[1,2,3]);

// 通过 bind 改变this的指向
let fn = print.bind(obj, 1,2,3);
fn();

再说一说这三者的共同点和不同点

共同点:

  • 功能角度:三者都能改变this指向,且第一个传递的参数都是this指向的对象
  • 传参角度:三者都采用的后续传参的方式

不同点:

  • 传参方面:call的传参是单个传递的,而apply后续传递的参数是数组形式(传单个值会报错),而bind没有规定,传递值和数组都可以
  • 执行方面:call和apply函数的执行是直接执行的,而bind函数会返回一个函数,然后我们想要调用的时候才会执行。

主要应用场景:

  1. call 经常用于继承
  2. apply 经常跟数组有关系,比如借助于数学对象实现数组最大值最小值
  3. bind 不调用函数,但是还想改变this指向,比如改变定时器内部的this指向

由于箭头函数没有自己的this指针,通过call()或者apply()方法调用一个函数时,只能传递参数(不能绑定this), 他们的第一个参数会被忽略

5. new

创建对象的几种方式?

  • 字面量
  • new
  • Object.create()

字面量

var obj={
    name:'lxy'
}
  • 代码量更少,更易读
  • 对象字面量运行速度更快。它们可以在解析的时候被优化,他不会像 new 一个对象一样,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为Object() 的函数就执行,如果没找到,就继续顺着作用域链往上找,直到找到全局Object() 构造函数为止
  • Object()构造函数可以接收参数,通过这个参数可以把对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是你想要的

new

问:new 内部发生了什么过程?可不可以手写实现一个 new 操作符?

对于new关键字,我们第一想到的就是在面向对象中new 一个实例对象,但是在JS中new和Java中的new的机制不一样

一般Java中,声明一个构造函数,通过new 类名() 来创建一个实例对象,而这个构造函数是一种特殊的函数。但是在JS中,只要new一个函数,就可以new一个对象,函数和构造函数没有任何的区别

对于new创建对象:

var arr = new Array();

new 的过程包括一下四个阶段:

  • 创建一个新对象。
  • 这个新对象的_proro_ 属性指向原函数的prototype属性。(即继承原函数的原型)
  • 将这个新对象绑定到此函数的this上
  • 返回新对象,如果这个函数没有返回其他对象

Object.create(null)

对于Object.create() 方式创建对象:

Object.create(proto,[propertiesObject]);
  • proto: 新创建对象的原型对象。

  • propertiesObject: (可选) 可为创建的新对象设置属性和值。

  • 一般用于继承:

    var People= function(name) {
        this.name = name;
    }
    People.peototype.sayName= function() {
        console.log(this.name);
    }
    function Person(name, age) {
        this.age = age;
        People.call(this, name); // 使用call, 实现了People属性的继承
    };
    // 使用Object.create()方法,实现People原型方法的继承,并且修改了constructor 指向
    Person.prototype = Object.create(People.peototype, {
        constructor:{
            configurable:true,
            enumerable: true,
            value:Person,
            writable:true
        }
    });
    Person.prototype.sayAge = function() {
        console.log(this.age);
    }
    var p1 = new Person('person1',25);
    
    p1.sayName() //'person1'
    p1.sayAge(); // 25
    

三者创建对象的区别

  • new 和字面量创建的对象的原型指向Object.prototype, 会继承object的属性和方法。
  • 而通过Object.create(null) 创建的对象,其原型指向null, null作为原型链的顶端,没有也不会继承任何属性和方法。

6. 闭包

什么是闭包?

闭包就是能够访问其他函数内部变量的函数

闭包的作用

  • 访问其他函数内部变量
  • 保护变量不被内存回收机制回收
  • 避免全局变量被污染,方便调用上下文的局部变量加强封装性

闭包的缺点

闭包长期占用内存,内存消耗很大,可能导致内存泄漏

如何避免闭包引起的内存泄漏

  • 在退出函数前,将不使用的局部变量全部删除。可以使变量赋值为null;

7. 内存泄漏,垃圾回收机制

什么是内存泄漏?

不再用到的内存,没有及时释放,就叫做内存泄漏。

为什么会导致内存泄漏?

**内存泄漏是指我们已经无法再通过js代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。**导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃

垃圾回收机制

问:怎么解决内存泄漏?说一说JS垃圾回收机制的运行原理?

需要我们手动管理好内存,但是对于JS有自动垃圾回收机制,自行对内存进行管理

两种垃圾回收策略

垃圾回收器主要的功能就是每隔一段时间,就去周期性的执行收集不再继续用到的内存,然后将其释放掉

标记清除法

它的实现原理就是通过判断一个变量是否再执行环境中被引用,来进行标记删除

引用计数法

引用计数的最基本的含义就是跟踪记录每个值被引用的次数。

  • 缺陷:两个对象的互相循环引用,在函数执行完成的时候,两个对象相互的引用计数并未归0,而是依然占据内存,无法回收,当函数执行多次时,内存占用就会变多,导致大量的内存得不到回收。

8. 原型链

原型:

每个JS对象都有_proto_ 属性,这个属性指向了原型

原型链:

原型链就是多个对象通过_proto_ 的方式连接了起来形成一条链

总结:

  • 所有的实例的_proto_ 都指向该构造函数的原型对象(prototype)。
  • 所有的函数(包括构造函数) 是Function() 的实例,所以所有函数的_proto_ 都指向Function() 的原型对象
  • 所有的原型对象(包括Function 的原型对象) 都是Object 的实例,所以_proto_都指向Object(构造函数) 的原型对象。 而Object 构造函数的 _proto_ 指向null
  • Function 构造函数本身就是Function的实例,所以_proto_ 指向Function的原型对象

9. 对象继承方法

继承

继承的核心思想就是,能够继承父类方法的同时,保证自己的私有属性和方法。

四个最常用的继承方式

原型继承

  • 核心思想:将父类的实例作为子类的原型
  • 优点:方法复用,由于方法定义在父类的原型上,复用了父类构造函数原型上的方法
  • 缺点:
    • 创建的子类实例不能传参
    • 子类实例共享了父类构造函数的引用属性

组合继承

  • 核心思想:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用
  • 优点:
    • 可传参:子类实例创建可以传递参数
    • 方法复用:同时所有的子类可以复用父类引用类型的共享
  • 缺点:
    • 组合继承调用了两次父类的构造函数,造成了不必要的消耗

寄生组合继承

  • 核心思想:组合继承+原型继承结合两者的优点
  • 优点:完美!
  • 缺点:无!
// 父类 
function Father(name){ 
    this.name = name;
    this.colors = ["red","blue","green"]; 
}
// 方法定义在原型对象上(共享) 
Father.prototype.sayName = function(){ 
    alert(this.name); 
};
function Son(name,age){ 
    Father.call(this,name); // 核心 
    this.age = age; 
}
Son.prototype = Object.create(Father.prototype); // 核心: 
Son.prototype.constructor = Son; // 修复子类的 constructor 的指向

ES6的 extend 继承

ES6 的extend 继承其实就是寄生组合式继承的语法糖。

  • 核心思想:

    • extends: 内部相当于设置了Son.prototype = Object.create(Father.prototype);
    • super() :内部相当于调用了Father.call(this)
  • 小结:

    • 子类只要继承父类,可以不写constructor, 一旦写了,则在constructor 中的第一句话必须是super.
    • 把父类当作普通方法执行,给方法传递参数,让方法中的this 是子类的实例
    class Son extends Father { // Son.prototype.__proto__ = Father.prototype
        constructor(y) { 
            super(200); // super(200) => Father.call(this,200) 
            this.y = y } }
    

10. 深浅拷贝

什么是深浅拷贝?

深浅拷贝是只针对Object和Array这样的引用数据类型的

  • 浅拷贝:只进行一层关系的拷贝,如果属性是基本类型,直接拷贝基本类型的值,如果属性值是内存地址,就拷贝这个地址,新旧对象公用一块内存
  • 深拷贝:进行无限层次的拷贝,会创造一个一摸一样的对象,不共享内存,修改对象不会互相影响

为什么要进行深浅拷贝?

let arr1 = arr2 = [1,2,3]
let obj1 = obj2 = {a:1, b:2, c:3}
arr1[0] = 2
obj1.a = 2
console.log(arr2[0]) // 2
console.log(obj2.a) // 2

从上面的代码可以看出:同一个Array或者Object赋值给两个不同变量时,变量指向的是同一个内存地址,改变其中一个变量的属性值,另一个也会改变。如果我们想要的是两个初始值相等但互不影响的变量,就要使用到拷贝。

深浅拷贝的使用

浅拷贝:

  • 扩展运算符(ES6新语法)

    let a = {c: 1}
    let b = {...a}
    a.c = 2
    console.log(b.c) // 1
    
  • Object.assign(target, source)

    将source的值浅拷贝到target目标对象上

    let a = {c: 1}
    let b = Object.assign({}, a)
    a.c = 2
    console.log(b.c) // 1
    

深拷贝:

  • JSON.stringify()

    let obj = {
        name: 'lxy',
        city: {
            city1: '北京',
            city2: '上海'
        }
    }
    // 浅拷贝
    let obj1 = {...obj}
    // 深拷贝
    let obj2 = JSON.stringify(obj)
    // 改变源对象的引用类型值
    obj.city.city1 = '杭州'
    console.log(obj1.city.city1) // 杭州
    console.log(JSON.parse(obj2).city.city1) // 北京
    

深浅拷贝的手动实现?

浅拷贝:

循环遍历对象,将对象的属性值拷贝到另一个对象中,返回该对象。

function shallowClone(o) {
    const onj = {};
    for(let i in o) {
        obj[i] = o[i]
    }
    return obj;
}

深拷贝:(简单实现)

对于深拷贝来说,就是在浅拷贝的基础上加上递归

var a1 = {
    b: {
        c: {
            d: 1
        }
    }
}
function deepClone(obj) {
    var target = {}
    for(var i in obj) {
        if(obj.hasOwnProperty(i)) {
            if(typeof obj[i] === 'object') {
                target[i] = deepClone(obj[i])
            } else {
                target[i] = obj[i]
            }
        }
    }
    return target
}

11. js事件循环机制

JavaScript是一门单线程非阻塞的脚本语言。

  • 单线程:代码执行时,都只有一个主线程来处理所有的任务,
  • 非阻塞:是指进行异步任务时,主线程会挂起这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。JavaScript引擎是通过 event loop (事件循环) 实现非阻塞的

主线程从任务队列读取事件,这个过程是循环不断地,所以整个运行机制又称为Event Loop(事件循环)

执行上下文

执行上下文是一个抽象的概念,可以理解为是代码执行的一个环境。分为全局执行上下文,函数(局部)执行上下文,Eval执行上下文

  • 全局执行上下文: this指向的是window,<script></script>标签中的代码
  • 函数执行上下文:每个函数调用的时候,会创建一个新的函数执行上下文
  • Eval执行上下文:不常用

执行栈

  • “栈”,一种数据结构。具有“先进后出”的特点。
  • 代码执行的时候,遇到一个执行上下文就将其依次压入执行栈中。
  • 先执行位于栈顶的执行上下文中的代码,当栈顶的执行上下文代码执行完毕就会出栈,继续执行下一个位于栈顶的执行上下文。
function foo() {
  console.log('a');
  bar();
  console.log('b');
}
function bar() {
  console.log('c')
}
foo()

代码解释:

  • 初始化状态,执行栈为空
  • foo(), foo函数执行,foo进入执行栈,console.log(‘a’),打印a
  • bar(), 执行函数bar,bar 进入执行栈,开始执行bar函数,console.log(‘c’) , 打印c
  • bar函数执行完毕,出栈,继续执行foo函数
  • console.log(‘b’) , 打印b, foo函数执行完毕。出栈。

宏任务

宏任务一般包括:

  • 整体的script标签内的代码
  • setTimeout
  • setInterval
  • setImmediate(Node)
  • I/o

微任务

微任务一般包括:

  • Promise
  • process.nextTick(Node)–所有异步任务之前触发(nextTick 队列会比 Promie 队列先执行。)
  • MutationObserver

循环机制的运行

  • 首先,事件循环机制是从<script>标签内的代码开始的(宏任务)
  • 在代码执行的过程中,如果遇到宏任务,如setTimeout, 就会将当前任务分发到宏任务队列中(队列,数据结构,先进先出)
  • 如果遇到微任务,如Promise, 在创建Promise实例对象时,代码顺序执行,如果遇到.then 任务,该任务就会被分配到微任务队列中
  • script标签内的代码执行完毕后,此时该宏任务执行完毕,然后去微任务队列执行所有的微任务
  • 微任务执行完毕,第一轮的消息循环执行完毕,页面进行一次渲染
  • 然后开始第二轮的消息循环,从宏任务队列中取出任务执行
  • 如果两个任务队列没有任务可执行了,此时所有的任务执行完毕

案例

console.log('1');
setTimeout(() => {
  console.log('2')
}, 1000);
new Promise((resolve, reject) => {
  console.log('3');
  resolve();
  console.log('4');
}).then(() => {
  console.log('5');
});
console.log('6')
// 打印顺序:1->3->4->6->5->2

代码解释:

  • 初始化状态,执行栈为空。
  • 首先执行 <script> 标签内的同步代码,此时全局的代码进入执行栈中,同步顺序执行代码,输出 1。
  • 执行过程中遇到异步代码 setTimeout (宏任务),将其分配到宏任务异步队列中。
  • 同步代码继续执行,遇到一个 promise 异步代码(微任务)。但是构造函数中的代码为同步代码,依次输出3、4,则 then 之后的任务加入到微任务队列中去。
  • 最后执行同步代码,输出 6。该宏任务执行完毕
  • 然后执行微任务队列里面的微任务
  • 微任务队列中只有一个微任务,所以输出 5。
  • 此时页面要进行一次页面渲染,渲染完成之后,进行下一次循环。
  • 在宏任务队列中取出一个宏任务,也就是之前的 setTimeout ,最后输出 2。
  • 此时任务队列为空,执行栈中为空,整个程序执行完毕。

12. Generator

Generator 是什么

  • generator函数是es6提供的一种异步编程的解决方案,可以理解成generator函数是一个状态机,封装了多个内部状态
  • 执行generator函数会返回一个迭代器对象,也就是说,generator函数除了是状态机还是一个迭代器对象生成函数
  • 返回迭代器对象,可以依次遍历generator函数内部的每一个状态

创建Generator函数

function* helloWorldGenerator() {
    yield 'hello'
    yield 'world'
    return 'ending'
}
var hw = helloWorldGenerator()
console.log(hw.next()); //  {value: "hello", done: false}
console.log(hw.next()); // {value: "world", done: false}
console.log(hw.next()); // {value: "ending", done: true}
console.log(hw.next()); // {value: undefined, done: true}
  • 每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,具备位置记忆的功能
  • yield表达式只能用在Generator函数里面
  • yield表达式如果用在另一个表达式之中,必须放在圆括号内
  • yield表达式用作函数参数或放在赋值表达式的右边

13. Promise

为什么会有Promise, Promise的诞生解决了哪些问题?

  • 由于JS的运行是单线程的, 所以当执行耗时的任务时,就会造成UI渲染的阻塞。当前的解决方法是使用回调函数来解决这个问题,当任务执行完毕,会调用回调方法
  • 回调函数存在以下缺点
    • 不能捕捉异常(错误处理困难) ——回调函数的代码和开始任务代码不在同一事件循环中
    • 回调地狱问题——promise链式调用
    • 处理并行任务棘手(请求之间互不依赖) ——promise.all

Promise用法

function fn() {
    return new Promise((resolve,reject)=>{
        // 成功时调用resolve(数据)
        // 失败时调用reject(错误)
    })
}
fn().then(success,fail).then(success1,fail1)
  • 通过内置的Promise构造函数可以创建一个Promise对象,构造函数中传入两个函数参数:resolve, reject。 两个参数的作用是,回调成功调用resolve, 回调失败调用reject
  • 调用Promise对象内置的方法 then, 传入两个函数,一个是成功回调的函数,一个失败回调的函数。当在promise内部调用resolve函数时,之后就会回调then方法里的第一个函数。当调用了reject方法时,就会调用then方法的第二个函数。
  • promise 相当于是一个承诺,当承诺兑现的时候(调用了resolve函数),就会调用then中的第一个回调函数,在回调函数中做处理。当承诺出现未知的错误或异常的时候(调用了reject函数), 就会调用then方法的第二个回调函数,提示开发者出现错误

Promise 的状态

其实Promise对象用作异步任务的一个占位符,代表暂时还没有获得但在未来获得的值。

Promise共有三种状态,完成状态和拒绝状态都是由等待状态转变的。一旦Promise进入了拒绝或完成状态,它的状态就不能切换了。

  • 等待状态(pending)
  • 完成状态(resolve)
  • 拒绝状态(reject): 显示拒绝(直接调用reject) 和隐式拒绝(抛出异常)

Promise 链式调用

Promise 可以实现链式调用,每次then之后返回的都是一个promise对象,可以紧接着使用then继续处理接下来的任务,这样就实现了链式调用。如果在then中使用了return, 那么return的值也会被Promise.resolve() 包装

Promise.all用法

Promise.all([promise1,promise2]).then(success,fail)

promise1和promise2都成功才会调用success

Promise.race用法

Promise.race([promise1,promise2]).then(success,fail)

promise1和promise2只要有一个成功的就会调用success

14. async/await

问:async 及 await 和 Generator 以及 Promise 什么区别?它们的优点和缺点分别是什么?await 原理是什么?

其实 ES7 中的 async 及 await 就是 Generator 以及 Promise 的语法糖,内部的实现原理还是原来的,只不过是在写法上有所改变,这些实现一些异步任务写起来更像是执行同步任务。

执行顺序

举个例子:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
async1()
console.log('script start')
// async1 start
// async2
// script start
// async1 end

特点

问:async 做一件什么事情?

带async关键字的函数,使得你的函数的返回值必定是promise对象。

  • 如果async关键字函数返回的不是promise,就自动用Promise.resolve()包装

  • 如果 async关键字函数函数显示地返回promise, 那就以你返回的promise为准

    async function fn1() {
        return 123
    }
    function fn2() {
        return 123
    }
    console.log(fn1()) // Promise {<resolved>:123}
    console.log(fn2()) // 123
    

问:await在等什么?

await 等的是右侧表达式的结果。

  • 右侧如果是函数,那么函数的return的值就是表达式的结果。

  • 右侧如果是一个’123’ 或者什么值,那表达式的结果就是’123’

问:await 等到之后,做了一件什么事情

  • 如果不是promise, await 会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为await表达式的结果
  • 如果它等到的是一个promise对象, await也会暂停async后面的代码,先执行async外面的同步代码,等着Promise对象fulfilled,然后把resolve的参数作为await表达式的运算结果

15. 模块化

为什么要使用模块化?

模块化解决了命名冲突问题,可以提高代码的复用率,提高代码的可维护性。

模块化的好处:

  • 可以解决命名冲突(减少命名空间污染)
  • 更好的分离,按需加载
  • 更高的复用性
  • 高可维护性

使用模块化的几种方式?

1. 函数:

最起初,实现模块化的方式是使用函数进行封装。将实现不同功能的代码封装到不同的函数中。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。

function a() {
    // 功能1
}
function b() {
    // 功能2
}

缺点:容易发生命名冲突或者数据的不安全性

2. 立即执行函数:

立即执行函数中的匿名函数中有独立的词法作用域,避免了外界访问此作用域的变量。通过函数作用域解决了命名冲突、污染全局作用域的问题

// module.js 文件
(function(window) {
    let name = 'JavaScript'
    function foo() {
        console.log(`name:${name}`)
    }
    // 暴露接口
    window.myModule = { foo }
})(window)
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.name = 'java' // 无法访问
    myModule.foo() // name:JavaScript
</script>

缺点:不能直接访问到内部的变量

3. CommonJS 规范:

CommonJS的规范主要用在Node.js中,为模块提供了四个接口:module、exports、require、global, CommonJS用同步的方式加载模块(服务器端),在浏览器端使用的是异步加载模块。

暴露模块:

  • module.exports = {}
  • exports.xxx = ‘xxx’
// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
// 对外暴露接口
module.exports = {
    counter: counter,
    incCounter: incCounter
}

引入模块:

主要通过require的方式引入, 但是require是Node的语法,在浏览器中无法识别

// 加载外部模块
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
// 原始类型的值被缓存,所以就没有被改变(commonJS 不会随着执行而去模块随时调用)
console.log(mod.counter) // 3

其它:

// 核心模块直接导入
const path = require('path');
// 路径模块
const m = require('./m.js');
// 自定义模块
const lodash = require('lodash')
  • 核心模块, 直接跳过路径分析和文件定位;
  • 路径模块, 直接得出相对路径就好了;
  • 自定义模块,先在当前目录的node_modules里找到这个模块,如果没有,它会往上一级目录查找查找上一级的node_modules, 依次往上,直到根目录下都没有,就抛出错误。

特点:

  • CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序

AMD和CMD

上面有CommonJS规范了,为什么还出AMD规范,因为CommmonJS是同步加载代码的,在浏览器中会发生堵塞问题,造成页面的无响应。所以浏览器不太适合使用CommonJS来加载。

CommonJS 规范对浏览器端和服务器端的不同之处

  • 服务器端所有的模块都存放在本地硬盘中,可以同步加载完成,等待的时间就是硬盘的读取时间。
  • 浏览器,所有的模块都放在服务器端,等待的时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态

AMD:

AMD(Asynchronous Module Definition), 即"异步模块定义"。 它主要采用异步方式加载模块,模块的加载不影响它后边语句的运行。所加载的模块,都会定义在回调函数中,加载完成,再执行回调函数

(1)使用方式

defined 是一个javascript库中的一个方法,使用之前需要安装一个库。

npm i requirejs

使用语法如下:

define(id, dependencies, factory)
  • id: 一个字符串,表示模块的名称
  • dependencies: 一个数组,是我们当前定义的模块要依赖于哪些模块,数组中的每一项表示的是要依赖模块的相对路径
  • factory: 工厂方法,具体模块内容

(2)导出模块

将add.js中的一个函数导出语法如下:

define(function() {
    var add = function(a,b) {
        return a+b;
    }
    return {
        add: add
    }
})

(3) 引入模块

导入上述导出的模块

var requirejs = require("requirejs"); // 引入 requirejs 模块
requirejs(['add'], funtion(math) {
    console.log(add.add(1,2))
})

CMD:

CMD(Common Module Definition), 主要是seajs的规范

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块

(1) 使用方式

// 所有模块都通过define 来定义
define(function(require, exports, module) {
    // 通过require 引入依赖
    var $ = require('jquery');
    var Spinning = require('./spinning')
    // 通过exports 对外提供接口
    exports.doSomething = ...
    // 或者通过module.exports 提供整个接口
    module.exports = ...
})
define(function (require, exports, module) {
    console.log('我比m1 要早加载。。。')
    var m1 = require('m1'); // 用到时才加载
    var add = function (a, b) {
        return a+b;
    }
    var print = function() {
        console.log(m1.name)
    }
    module.exports = {
        add: add,
        print: print
    }
})

AMD和CMD的区别

  • AMD依赖前置, js很方便的就知道要加载的是哪个模块了,因为已经在define的dependencies参数中就定义好了,会立即加载它
  • CMD是就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。只有在用到某个模块的时候再去require

ES6 Moudle

ES6实现的模块就非常简单,用于浏览器和服务器端。import命令会被Javascript引擎静态分析,在编译时就引入模块代码。

(1) export导出模块

  • 命名式导出
  • 默认导出
// 命名式导出
// 方式一
const a = 1;
export { a };
// 方式二
export const a = 1;
// 方式三(as 重命名导出)
const a = 1;
export {a as A}

// 默认导出
const a = 1;
export default a;
// 等价于
export {a as default}

(2) import 导入模块

  • 命名式导入
  • 默认导入
// 默认导入
import { a } from './module';
// 重新命名
import { a as A} from './module';
// 只想要运行被加载的模块
import './module';
// 整体加载
import * as module from './module'
// default接口和具名接口
import module, { a } from './module'

ES6和CommonJS的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6模块输出的是值的引用
    • 所谓值的拷贝,原始类型的值被缓存,不随模块内部的改变而改变。
    • ES6模块是动态引用,不缓存值, 模块内外是绑定的,而且是只读引用,不能修改值。ES6的js引擎对脚本静态分析的时候,遇到加载命令模块import, 就会生成一个只读引用, 当真正用到模块里面的值的时候,就会去模块内部去取。
  • CommonJS 模块是运行时加载, ES6模块是编译时加载输出接口
    • 运行时加载:CommonJS模块就是对象;是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为运行时加载
    • 编译时加载:ES6模块不是对象,而是通过export命令显式指定输出的代码。import时采用静态命令的形式,即在import指定加载某个输出值,而不是加载整个模块,这种加载称为编译时加载。
  • CommonJS导入的模块路径可以是一个表达式,因为它使用的是require() 方法;而ES6 Modules只能是字符串
  • CommonJS this 指向当前模块, ES6 Modules this 指向undefined

16. 防抖节流

认识防抖节流

JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。而对于某些频繁的事件处理会造成性能的损耗,可以通过防抖和节流来限制事件频繁发生

为什么需要防抖节流

为什么需要防抖?

举个例子:

比如想要搜索一个MacBook:

  • 当输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
  • 当继续输入ma时,再次发送网络请求;
  • 那么macbook一共需要发送7次网络请求;
  • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;

但是我们需要这么多次的网络请求吗?

  • 不需要,正确的做法应该是在合适的情况下再发送网络请求;
  • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;
  • 比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;
  • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;

这就是防抖的操作:

  • 只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

  • 当事件密集触发时,函数的触发会被频繁的推迟;

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

防抖的应用场景:

  • 输入框中频繁的输入内容,搜索或者提交信息
  • 频繁的点击按钮,触发某个事件
  • 监听浏览器滚动事件,完成某些特定操作
  • 用户缩放浏览器的resize事件

总之,密集的事件触发,我们只希望触发比较靠后发生的事件,就可以使用防抖函数;

为什么需要节流?

节流:在某个时间内(比如500ms),某个函数只能被触发一次;

节流的应用场景:

  • 监听页面的滚动事件
  • 鼠标移动事件
  • 用户频繁点击按钮操作

总之,依然是密集的事件触发,但是这次密集事件触发的过程不会等到最后一次才进行函数调用,而是会按照一定的频率进行调用

本文地址:https://blog.csdn.net/qq_46178261/article/details/112858172

《前端面试——JS进阶.doc》

下载本文的Word格式文档,以方便收藏与打印。