JavaScript基础知识总结


1. 作用域和作用域链

作用域:变量和函数的有效访问范围。分为全局作用域函数作用域块作用域
作用域链:当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

其他相关概念:
💡 变量对象
JavaScript执行一段可执行代码时,会创建对应的执行上下文,变量对象是与 执行上下文 相关的数据作用域,存储了在上下文中定义的 变量函数

💡变量提升
使用var声明的变量,无论其实际声明的位置在何处,都会被视为声明于所在函数的顶部(如果声明不在函数内,则视为在全局作用域的顶部),这就是变量提升。总结来看:

  • JS 会将变量的声明提升到JS顶部执行,因此对于这种语句:var a = 2;其实上js会将其分为var a;a = 2;两部分,并且将var a;这一步提升到顶部执行。
  • 变量提升的本质其实是由于JS引擎在编译的时候,就将所有的变量声明了,因此在执行的时候,所有的变量都已经完成声明。
  • 当有多个同名变量声明的时候,函数声明会覆盖其他的声明。如果有多个函数声明,则是由最后的一个函数声明覆盖之前所有的声明。
  • letconst不存在变量提升(有些文章说letconst也会提升,这个值的商榷)

💡暂时性死区(TDZ)
JS 引擎编译代码块并发现变量声明时,它会在面对 var 的情况下将声明提升到函数或全局作用域的顶部,而面对 letconst 时会将声明放在暂时性死区内。通常使用暂时性死区用来描述letconst在达到声明处之前都是无法访问的这一特性。

2. Promise

基本概念

PromiseES6 新增的语法,是一种异步编程的解决方案,Promise 本质上是一个绑定了回调的对象。 Promise 在一定程度上解决了回调函数的书写结构问题,解决了回调地狱的问题。Promise 可以看作是一个状态机,它有三种状态:pendingfulfilledrejected,初始状态是 pending,可以通过函数 resolve() 把状态变为 fulfilled,或者通过函数 reject() 把状态变为 rejected,状态一经改变就不能再次变化。

常用API

  • Promise.all(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例。只有受监视的所有Promise实例都完成,返回的Promise实例才会完成,只要有一个被拒绝,返回的Promise实例就会被立即拒绝。

  • Promise.race(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例,只要有一个被解决(完成或拒绝),返回的Promise实例就会被立即解决(完成或拒绝)。

  • Promise.any(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例,只要有一个被完成,返回的Promise实例就会变成完成状态,如果所有的promise实例都被拒绝,返回的promise实例就会变成拒绝状态。

  • Promise.allSettled(): 接受多个Promise实例组成的数组作为唯一参数,返回一个Promise实例。当所有Promise实例都解决时,返回的Promise会被完成,完成的结果是一个包含所有promise结果的对象数组。

实现原理

💡Promise实现原理
💡Promise简易实现

promise取消实现

  • 借助Promise.reject(),在封装promise的时候,对外暴露一下promise中的reject方法
  • 借助Promise.race()的特性,将一个辅助promise的reject暴露出来,调用辅助promise的reject即可让Promise.race()提前结束执行

💡如何取消一个正在执行的Promise

3. async/await

async/awaitGenerator 函数的语法糖,作用是用同步的方式,执行异步操作,最常见的用法就是用同步的写法来替代promise.then()的嵌套问题。

// 后续异步请求都依赖于前一个请求的结果
function fn() {
    request().then(res1 => {
        request(res1).then(res2 => {
            request(res2).then(res3 => {
                // ...
            })
        })
    })
}


// 替代写法
async function fn() {
    const res1 = await request()
    const res2 = await request(res1);
    const res3 = await request(res2);
    // ...
}

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
实现

async function fn(args) {
  // ...
}

// 等同于
function fn(args) {
    // spawn是自动执行器函数,自动调用generator的next方法
    return spawn(function* () {
        // ...
    });
}

// spawn函数实现
function spawn(genF) {
    // async函数最终返回的是一个promise
    return new Promise(function(resolve, reject) {
        const iterator = genF();
        function step(nextF) {
            let next;
            try {
                next = nextF();
            } catch(e) {
                return reject(e);
            }
            // next.done为true时,说明走到了iterator的最后一步,直接返回结果
            if(next.done) {
                return resolve(next.value);
            }
            // next.value有可能是常量或promise,所以这里采用Promise.resolve处理
            Promise.resolve(next.value).then(function(v) {
                // 递归调用step
                step(function() { 
                    return iterator.next(v); 
                });
            }, function(e) {
                step(function() { 
                    return iterator.throw(e); 
                });
            });
        }
        step(function() { return iterator.next(); });
    });
}

4. ES6常用新特性

💡 ES6常用特性总结

5. 数据类型的判断方法

💡JavaScript数据类型判断

6. 继承

💡JavaScript中的继承

7. 原型链

💡JavaScript原型与原型链

8. 闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0](); // 3
data[1](); // 3
data[2](); // 3

// 改善输出结果
for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
    return function() {
        console.log(i);
    }
  })(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2

9. 事件循环机制

  1. JS引擎逐行扫描JS代码,遇到同步任务会将其添加到JS引擎线程的执行栈中。
  2. 遇到异步任务,交由浏览器渲染进程中的相应线程(比如setTimeout交给定时器线程http请求交给异步网络请求线程)管理,当异步任务有了运行结果,事件触发线程就会将异步任务的事件回调依次放进任务队列的末尾。该线程维护一个微任务队列和一个宏任务队列
  3. 执行栈中的任务执行完之后,JS引擎会从宏任务队列队首取一个宏任务到执行栈中开始执行;
  4. 宏任务执行完毕后,JS引擎会依次从当前微任务队列中取出微任务到执行栈中执行;
  5. 当前微任务队列为空,回到3,如此循环执行

💡JavaScript之事件循环

10. 浅拷贝和深拷贝

浅拷贝

拷贝时,只是拷贝了基本类型的值,而引用类型的值,复制的是引用类型的值的地址,该值指向同一个内存地址,会互相影响。

实现方式一: Object.assign(target, source1, source2)

const obj = {
  a: '123',
  b: 234,
  c: true,
  d: null
}

const newObj = Object.assign({}, obj);

实现方式二: ES6扩展运算符

const obj = {
  a: '123',
  b: 234,
  c: true,
  d: null
}

const newObj = {...obj};

实现方式三: 递归

function shallowClone(obj) {
  // 判断拷贝的obj是对象还是数组
  const objClone = Array.isArray(obj) ? [] : {};
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
            objClone[key] = obj[key]; // 直接拷贝
        }
    }
    return objClone;
}

实现方式四:数组

  • Array.concat()
  • Array.slice()

深拷贝

拷贝后的对象与原来的对象是完全隔离,即使是引用类型的属性,也不会相互影响。
实现方式一: JSON.stringify序列化和JSON.parse反序列化
缺点:对象中undefinedfunctionsymbol这三种类型的值会被过滤掉

const obj = {
  a: '123',
  b: 234,
  c: true,
  d: null,
  e: function() {console.log('test')},
  h: new Set([4,3,null]),
  i: Symbol('fsd'),
  k: new Map([ ["name", "test"],  ["title", "Author"]  ])
}
console.log(JSON.stringify(obj)); // {"a":"123","b":234,"c":true,"d":null,"h":{},"k":{}}

const newObj = JSON.parse(JSON.stringify(obj));

实现方式二: 递归

function deepClone(obj) {
  // 判断拷贝的obj是对象还是数组
  const objClone = Array.isArray(obj) ? [] : {};
    if (obj && typeof obj === "object") { // obj不能为空,并且是对象或者是数组,因为null也是object
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                if (obj[key] && typeof obj[key] === "object") { // obj里面属性值不为空并且还是对象,进行深度拷贝
                    objClone[key] = deepClone(obj[key]); // 递归进行深度的拷贝
                } else {
                    objClone[key] = obj[key]; // 直接拷贝
                }
            }
        }
    }
    return objClone;
}

11. this指向

JavaScript中的this

12. 箭头函数与普通函数的区别

  • 没有thissuperargumentsnew.target绑定,thissuperargumentsnew.target的值由所在的、最靠近的非箭头函数来决定;
  • 不能被使用new调用;
  • 没有原型;
  • 不能更改this
  • 不允许重复的具名参数,传统的函数中参数可以重名。

13. 防抖与节流

防抖

指定时间内函数多次调用都会被重置,只会在最后一次触发结束后延时执行
常用使用场景:

  • 按钮多次点击(发送验证码按钮、提交按钮、支付按钮等)
  • input输入框输入事件
/**
 * @fn : 要执行的函数
 * @delay : 执行函数的时间间隔(毫秒)
 */ 
 
function debounce(fn, delay) {
    let timer;
    return function(...args) {    
        timer && clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay)
    }
}

节流

指定时间内,函数多次调用只执行一次
常用使用场景:

  • 浏览器窗口resizescroll事件
  • 鼠标移动mousemove事件
  • 上传进度事件
  • 文档编辑隔一段时间自动保存
/**
 * @fn : 要执行的函数
 * @delay : 每次函数的时间间隔
 */  
function throttle(fn, delay) {
    let timer;    // 定时器
 
    return function(...args) {
        if(timer) return;
        timer = setTimeout(() => {
            timer = null;
            fn.apply(this, args);
        }, delay);
    }
}

在线演示DEMO

14. instanceof原理

function instance_of(left, right) {
  const RP = right.prototype; // 构造函数的原型
  while(true) {
    if (left === null) return false;
    if (left === RP) return true;
    left = Object.getPrototypeOf(left); // 沿着原型链重新赋值
  }
}

15. =====

  • ==!= — 先转换数据类型再比较
  • ===!== — 仅比较而不转换数据类型

==!= 遵循下列转换规则:

  • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值:false转换为0,true转换为1
  • 一个是字符串,一个是数值,则在比较相等性之前先将字符串转换为数值
  • 一个是对象,一个不是对象,则调用对象的 valueOf() 方法,得到基本数据类型后再按前面的规则比较
  • nullundefined 不能转换为其他任何值

==!= 在比较时需要遵循下列规则:

  • nullundefined 是相等的
  • NaN 与任何值都不相等,包括它自己
  • 两个操作数都是对象,如果两个操作数指向同一个对象,则为true,否则为false

特殊情况比较结果:

表达式
null == undefined true
NaN == NaN false
false == 0 true
true == 1 true
true == 2 false
undefined == 0 false
null == 0 false

16. call、bind、apply原理

MDN:
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

/**
 * call的模拟实现
 */
Function.prototype.myCall = function(context) {
    // this指向调用myCall方法的函数
    if (typeof this !== 'function') { 
        throw new TypeError('Error, caller must be a function') 
    }
    context = context || window;

    // 将调用myCall的函数赋值为context对象的方法
    context.fn = this;
    const args = [...arguments].slice(1);
    // 利用context.fn()来执行原函数,原函数中的this就指向了context
    const result = context.fn(...args) ;
    delete context.fn ;
    return result;
}

MDN:
apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

/**
 * apply的模拟实现
 * 原理与call一样,区别仅在于参数的处理上
 */
Function.prototype.myApply = function(context) { 
    if (typeof this !== 'function') { 
        throw new TypeError('Error') 
    } 
    context = context || window;
    context.fn = this;  
    let result // 处理参数和 call 有区别  
    if (arguments[1]) { 
        result = context.fn(...arguments[1]) 
    } else { 
        result = context.fn() 
    } 
    delete context.fn;
    return result; 
}

MDN:
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

/**
 * bind的模拟实现
 * 
 */
Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    const _this = this
    // 保存调用bind时传递的参数
    const args = [...arguments].slice(1)
    // 返回一个函数
    return function F() {
        // 因为返回了一个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
            return new _this(...args, ...arguments)
        }
        // 修改this及合并参数
        return _this.apply(context, args.concat(...arguments))
    }
}

17. new的原理

使用 new 来调用函数,会自动执行下面的操作:

  1. 创建一个空对象{}
  2. 将这个新对象的原型对象指向构造函数的原型对象,以继承原型上的方法
  3. 将构造函数中的this指向到新创建的对象并执行构造函数,以获取实例属性
  4. 如果构造函数执行后返回了对象,就将该对象作为结果返回;否则就将上面创建的新对象作为结果返回
// 简单实现
function myNew(Fn, ...args) {
    // 原型式继承
    const obj = Object.create(Fn.prototype);
    // 执行构造函数
    const res = Fn.apply(obj, args);
    return res instanceof Object ? res : obj;
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    return this;
}

const student1 = myNew(Person, 'Mike', 20);
console.log(student1); //  {"name":"Mike","age":20}

18. 事件委托

事件传播的 3 个阶段:

  • 捕获阶段(Capturing phase)—— 从window,document 和根元素开始,事件向下扩散至目标元素的祖先
  • 目标阶段(Target phase)—— 事件到达目标元素
  • 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡,一直到根元素,document 和 window。

为何使用事件委托?

在 JavaScript 中,页面内事件处理程序的个数会直接影响页面的整体性能,因为每个事件处理程序都是对象,对象会占用内存,内存中的对象越多,页面的性能则越差。此外,事件处理程序需要与 DOM 节点进行交互,访问 DOM 的次数越多,引起浏览器重绘和重排的次数也就越多,从而影响页面的性能。

事件委托实现原理

事件委托是利用事件的冒泡原理来实现的,大致可以分为三个步骤:

  1. 确定要添加事件元素的父级元素;
  2. 给父元素定义事件,监听子元素的冒泡事件;
  3. 使用 event.target 来定位触发事件冒泡的子元素。

19. Generator(生成器)和 Iterator(迭代器)

迭代器和生成器

20. 纯函数

定义

一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数

// 纯函数
function sum1(a, b) {
 return a + b
}

// 非纯函数,因为函数执行过程中改变了message的值,被认定产生了副作用
let message = 'hello'
function sum2(a, b) {
 message = 'hi'
 return a + b
}

const nums = [1, 2, 3, 4, 5]
// slice是纯函数
const newNums1 = nums.slice(1, 3)

// splice不是纯函数,因为它改改变了原数组的值
const newNums2 = nums.splice(1, 3);

纯函数的价值

纯函数非常靠谱,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。虽然我们的程序不可避免的会产生副作用,比如 HTTP 调用、IO 操作等,但尽可能多的地方使用纯函数将大大提高程序的可读性、可调试性和可测试性,代码重构时也不必担心没注意到的副作用而搞乱了整个应用。
纯函数在函数式编程中被大量使用,而且诸如 ReactJS 和 Redux 等优质的库都要求使用纯函数。
正确地使用纯函数可以产生更加高质量的代码,并且也是一种更加干净的编码方式。

21. Ajax、Axios和fetch的区别

  • Ajax是指一种无需刷新页面即可向服务器请求数据的技术。核心是XMLHttpRequest对象。
  • Axios是通过 Promise 实现 XHR 封装的 JavaScript 库,在浏览器和Node.js环境中都可以运行。
  • fetchJavaScript提供的原生API,是Ajax的替代方案,没有使用XMLHttpRequest对象。只可以在浏览器上使用。

22. 常用的设计模式

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
应用场景:弹窗组件同一时间只能展示一个

观察者模式

一个对象(观察者)订阅另一个对象(主题),当主题被激活的时候,触发观察者里面的事件。
应用场景:Vue响应式原理中的DepWatcher

发布-订阅模式

发布订阅模式与观察者模式相比,发布订阅模式中有三个角色,发布者 Publisher ,事件调度中心 Event Channel ,订阅者 Subscriber
订阅者把自己想要订阅的事件注册到调度中心,当发布者发布事件到调度中心,再由调度中心统一调度订阅者注册到调度中心的处理代码。
应用场景:Vue中的$on,$emit用来组件间通信

23. 发布订阅模式和观察者模式

  • 角色上看
    • 观察者模式里只有两个角色:观察者和被观察者。
    • 发布订阅模式里有三种角色:发布者、订阅者、调度器(第三者)。
  • 耦合关系上看
    • 观察者和被观察者是松耦合的关系,两个主体存在直接交互
    • 发布者和订阅者则完全不存在耦合,两个主体是借助第三方主体来进行间接交互

24. JS的模块化规范

📕JavaScript模块化规范总结

25. 传值与传址

  • 传值:在赋值过程中,首先对值进行了一份拷贝,而后将这份拷贝存储到一个变量、对象属性或数组元素中。拷贝的值和原始的值是完全独立、互不影响的。当一份数据通过值传递给一个函数,实际上被传递的不是数据本身,而是数据的一份拷贝。因此,如果函数修改了这个值,影响到的只是数据的那份拷贝,而并不影响数据本身。
  • 传址:在赋值过程中,变量实际上存储的是数据的地址(对数据的引用),而不是原始数据或者是数据的拷贝。如果值通过一个地址发生了改变,这个改变也会通过原始地址表现出来。

文章作者: Snail-Lu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Snail-Lu !
  目录