第二章. js进阶【必备篇】

2,416 阅读11分钟

整理中高级前端系列,可以当作面试复习,也可以当作实战来看,分享一下 方便自己,方便他人。有不足的地方欢迎评论~

第一趴:css进阶

第二趴:js进阶

第三趴:vue框架进阶

第四趴:工程化

专业术语

  • 常量、变量、数据类型
  • 形参、实参
  • 匿名函数、具名函数、⾃执⾏函数
  • 函数声明、函数表达式
  • 堆、栈
  • 同步、异步、进程、线程

执行上下文

当函数执行时,去创建一个称为「执行上下文(execution contex)」的环境,分为 创建和执行 两个阶段

创建阶段

是指 函数被调用但未被执行任何代码时,此时创建了一个拥有3个属性的对象:

代码执行阶段

主要工作:1、分配变量、函数的饮用、赋值   2、执行代码

作用域

js中有全局作用域、函数作用域,es6中又增加了 块级作用域。 作用域最大的用途就是 隔离变量或函数,并控制他们的生命周期作用域是在函数执行上下文创建时定义好的不是函数执行时定义的。

作用域链

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。如果在当前作用域无法找到会向上一级作用域寻找,直到找到或抵达全局作用域,这样的链式关系就是 作用域链(Scope Chain)

闭包

可以访问其他函数作用域中(内部)变量的函数

  • 可以读取函数内部的变量
  • 让这些变量始终保持再内存中,即:闭包可以使它诞生的环境一直存在。

平时怎么用,应用场景链接

什么节点循环绑定事件,解决方案 立即执行函数、var => let、

this的5种场景

1、函数直接调用时

function myfunc() {
	console.log(this) // this是widow
}
var a = 1;
myfunc();

// 常考点
function show () {
    console.log('this:', this); // this是window
}
var obj = {
    show: function() {
        show(); // 函数被直接调用了
    }
};
obj.show()

2、函数被别人调用时

function myfunc(){
	console.log(this) // this是对象a
}
var a = { myfunc: myfunc }
a.myfunc();

3、new一个实例时(构造函数)

function Penson(name) {
  this.name = name;
	console.log(this); // this是实例p
}
var p = new Penson('this:::');

4、apply、call、bind

function getColor(color) {
  this.color = color;
  console.log(this);
}
function Car(name, color){
  this.name = name; // this指的是实例car
  getColor.call(this, color); // 这⾥的this从原本的getColor,变成了car
}
var car = new Car('卡⻋', '绿⾊');

5、箭头函数时

// 箭头函数
var a = {
  myfunc: function() {
    setTimeout(() => {
    console.log(this); // this是a
    }, 0)
  }
};
a.myfunc();

最强总结:一字一句都是重点

1、对于直接调⽤的函数来说,不管函数被放在了什么地⽅this都是window 2、对于被别⼈调⽤的函数来说,被谁点出来的,this就是谁 3、在构造函数中,类中(函数体中)出现的 this.xxx = xxx 中的 this是当前类的⼀个实例 4、call、apply时,this是第⼀个参数。bind要优于call/apply哦,call参数多,apply参数少 5、箭头函数没有⾃⼰的this,需要看其外层是否有函数,如果有,外层函数的this就是内部箭头函数 的this,如果没有,则this是window

call、bind、apply

call()、apply()、bind() 都是用来重定义 this 这个对象的!

  • call:的参数是直接放进去obj.myFun.call(db,'string1', ... ,'stringN' )
  • apply:参数都放在一个数组里面传进去 obj.myFun.apply(db,['成都',...,'string' ])
  • **bind:返回是函数,需要自己再执行,它 的参数和 call **一样

面向对象

构造函数

本身就是一个函数,为了规范一般将首字母大写,区别在于 使用 new 生成实例的函数就是构造函数,直接调用的就是 普通函数。

prototype原型

每个对象都有其原型对象,对象从原型那继承属性和方法,这些属性和方法定义在 对象的构造函数的 prototype 属性上,而非对象实例本身。

原型链继承

深拷贝 和 浅拷贝

来源于赋值(址)

  • 基本数据类型 **赋值 **之后两个变量互不影响
  • 引用类型 赋址 ,两个变量具有相同得引用,指向同一个对象,相互影响(所以才出现了 深拷贝和浅拷贝)

浅拷贝

简单来讲,浅拷贝 之解决了 第一层问题 基本类型值和引用类型地址

  • Object.assign() :

展开语法 Spread ...

let a = {
    name: "ceshi",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = {...a};
console.log(b);
// {
//  name: "ceshi",
//  book: {title: "You Don't Know JS", price: "45"}
// }

深拷贝

拷贝所有属性以及指向得动态分配的内存。拷贝前后两个 对象互不影响。

  • JSON.parse(JSON.stringify(object)) 拷贝对象没问题,但是 数组有问题:undefined、symbol 和函数这三种情况,会直接忽略。
  • 所以 诞生了 lodash.cloneDeep()

总结

-- 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

如何实现一个深拷贝

思路:浅拷贝 + 递归,浅拷贝时 判断属性值是否是对象,是对象就进行递归操作。

export function deepClone(source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments', 'deepClone')
  }
  const targetObj = source.constructor === Array ? [] : {}
  Object.keys(source).forEach(keys => {
    if (source[keys] && typeof source[keys] === 'object') {
      targetObj[keys] = deepClone(source[keys])
    } else {
      targetObj[keys] = source[keys]
    }
  })
  return targetObj
}

高阶

防抖和节流

防抖:debounce

某个函数在一段时间内无论被触发了多少次,都只执行最后一次。(会延长触发时间)

实现原理是利用定时器,函数第一次执行时设置一个定时器,调用时 发现已有定时器那么清空之前定时器从新设置一个新的定时器,直到最后一个定时器到时触发函数执行

function debounce(fn, awit = 50) {
  let timer = null
  return function(...args) {
    // 存在定时器则清空
    if (timer)  clearTimeout(timer)
    // 第一次设置定时器
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, awit);
  }
}
const ceshiFnDeb = debounce(()=> console.log('ceshiFnDeb防抖函数执行了'), 1000)
document.addEventListener('scroll', ceshiFnDeb)

节流:throttle 适合用于 函数被频繁调用的场景

函数节流指的是 某个函数在 指定时间内(n秒)只执行一次,不管后面函数如何调用请求,不会延长时间间隔,n秒间隔结束后 第一次遇到 新的函数调用会触发执行,以此类推。

// 节流 throttle: 固定间隔时间内只调用一次函数
// 思路1:根据两个时间戳来对比
function throttle (fn, awit = 50) {
  let startTime = 0
  return function(...args){
    let nowTime = +new Date()
    // 当前时间和上次时间做对比,大于间隔时间的话,就可以执行函数fn
    if (nowTime - startTime > awit) {
      // 执行完函数之后重置初始时间,等于最后一次触发的时间
      startTime = nowTime
      fn.apply(this, args)
    }
  }
}
// 执行 throttle 函数返回新函数
const ceshiFn = throttle(()=> console.log('ceshiFn节流函数执行了'), 2000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(ceshiFn, 10);

不过市面上比较全的还是 lodash的防抖节流函数 **

事件循环Event Loop

宏任务 MacroTask

script全部代码、setTimeOut、setInterval、I/O、

微任务MicroTask

Promise、Process.nextTick(Node独有)

这里归纳3个重点

  1. 首先执行宏任务队列script,一次只从队列中去一个任务执行,执行完后 去执行微任务队列中的任务;
  2. 微任务队列中所有的任务会被 依次取出来执行,直到microtask queue为空
  3. 期间要执行 UI rendering,它的节点是在执行完所有 微任务之后,下一个宏任务之前,紧跟着执行UI render

Promise对象 - 手写

本身是一个构造函数,自身有 all、reject、resolve方法,原型上有 then、catch方法

promise的then可以接受两个函数,第一个resolve,第二个参数为reject

  • resolve:将promise的状态padding变为 fullfiled已完成,一般用来进行业务代码处理
  • reject:将状态padding变为 rejected已拒绝,一般用来进行拦截报错处理
  • then:就是把原来的回调写法分离出来,在异步操作Async执行完后,用链式调用的方式执行回调函数
  • all:接受一个数组参数,并行 执行异步操作的能力,在所有异步操作执行完之后才执行回调then,all会把所有异步操作的结果放进一个数组中传给then。

async 和 await

函数返回一个 Promise对象,如果内部发生异常则会导致返回的 Promise 对象状态变为reject状态。抛出的错误会被catch方法接收到。

async表示函数里有异步操作

async函数内部 return 返回的值。会成为then方法回调函数的参数。

await 表示紧跟在后面的表达式需要等待结果(同步)

正常情况下,await 命令后面跟着的是Promise对象,返回对象的结果,如果不是的话,就直接返回对应的值。

Proxy与Object.definproperty()

Object.defineProperty()

Vue 2.x 利用 Object.defineProperty(),并且把内部解耦为 Observer, Dep, 并使用 Watcher 相连

  1. 不能监听数组的变化,这些方法无法触发set:push、pop、shift、unshift、splice、sort、
  2. 必须遍历对象的每个属性
  3. 必须深层遍历嵌套的对象

Proxy

Vue 在 3.x 版本之后改用 Proxy 进行实现

  1. 针对整个对象而不是对象的某个属性
  2. 支持数组
  3. 嵌套支持 是 get里面递归调用Proxy 并返回

模块化编程

为了解决两大痛点: 1、每个模块都要有自己的 变量作用域,两个模块之间的内部变量不会产生冲突。 2、不同模块之间保留相互 导入和导出的方式方法,模块之间能够相互通信,模块的执行与加载遵循一定的规范,能保证彼此之间的依赖关系

模块化开发的4个好处:

  1. 避免变量污染,命名冲突
  2. 提高代码复用率
  3. 提高 维护性
  4. 方便管理依赖关系

CommonJS

是服务器端模块的规范,Node.js采用了这个规范。

每个JS文件就是一个模块(module),每个模块内部使用 require 函数和 module.exports 对象来对模块进行导入和导出

AMD 异步模块定义

适合web开发的模块化规范

模块文件中,我们使用 define 函数定义一个模块,在回调函数中接受定义组件内容。这个回调函数接受一个 require 方法,能够在组件内部加载其他模块,这里我们分别传入 模块ID,就能加载对应文件内的AMD模块。

// moduleA.js
define(function(require) {
var m = require('moduleB');
setTimeout(() => console.log(m), 1000);
});

// moduleB.js
define(function(require) {
var m = new Date().getTime();
return m;
});

// index.js
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
console.log(moduleB);
});

CMD 通用模块定义

UMD

同时被 CommonJs规范和AMD规范加载的UMD模块,一套同时适用于node.js和web环境

UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。 在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

原生JS模块化(ES6)

ES6 为导入(importing)导出(exporting)模块带来了很多可能性

与前两者的最大区别在于,ESModule是由JS解释器实现,而后两者是 在宿主环境中运行时实现。ESModule导入实际上实在语法层面新增了一个语句,而AMD和ComminJs加载模块实际上是调用了 require函数。

image.png

正则表达式

正则表达式可以从一个基础字符串中根据一定的匹配模式替换文本中的字符串、验证表单、提取字符串等等。

最全正则表达式链接

正则表达式主要依赖于元字符。 元字符不代表他们本身的字面意思,他们都有特殊的含义。一些元字符写在方括号中的时候有一些特殊的意思。以下是一些元字符的介绍:

元字符 描述
. 句号匹配任意单个字符除了换行符。
[ ] 字符种类。匹配方括号内的任意字符。
[^ ] 否定的字符种类。匹配除了方括号里的任意字符
* 匹配>=0个重复的在*号之前的字符。
+ 匹配>=1个重复的+号前的字符。
? 标记?之前的字符为可选.
{n,m} 匹配num个大括号之间的字符 (n <= num <= m).
(xyz) 字符集,匹配与 xyz 完全相等的字符串.
| 或运算符,匹配符号前或后的字符.
\ 转义字符,用于匹配一些保留的字符 [ ] ( ) { } . * + ? ^ $ \ |
^ 从开始行开始匹配.
$ 从末端开始匹配.

ES6篇请移步-链接

第一趴:css进阶

第二趴:js进阶

第三趴:vue框架进阶

第四趴:工程化

同学觉得有帮助的可以点个赞,以示鼓励~ 😊