JavaScript

2022/6/26

# 1. 同步和异步

同步:只有当前调用的方法执行完成并返回结果后,才能执行后续代码
异步:不用等待调用方法完成,而是直接执行后续代码

# 2. 数据类型

  • 简单数据类型:Number、String、Boolean、null、undefined、Symbol、bigInt
  • 复杂数据类型(引用数据类型):Object

# 存储方式

  • 简单数据类型存储在栈中,大小固定,占据空间小
  • 复杂数据类型存储在堆中,并在栈中存储了对应的引用地址,指向其在堆中的地址,大小不确定,一般占据空间大。

栈和堆的区别:
栈比堆内存小,但运行速度快; 堆是无序存储,根据引用获取

# 判断数据类型

typeof [] === typeof {} === typeof null === 'object'  
typeof undefined === 'undefined'  
typeof 123 === typeof NaN === 'number'  
typeof '' === 'string'  

判断是否为Array:Array.isArray(arr);
判断是否为null:val === null
null === null 结果为 true
undefined === undefined 结果为 true
NaN === NaN 结果为 false,要使用 Number.isNaN(num) 来判断是否为 NaN null == undefined 结果为true,null只有与undefined、null 做 == 返回值会为true
Boolean([])、Boolean({}) 值为 true

# 3. 正则表达式 (RegExp) (opens new window)

创建正则表达式的两种方法:

  1. var reg = new RegExp('\b+abc');
  2. var reg = /\b+abc/;

\d 匹配数字, \s 匹配空格,+ 表示至少一个,*表示任意个(包括0),[] 表示范围,^ 表示以什么开头,$ 表示以什么结尾,? 表示 0 或 1 个

# 4. 数组方法


  1. push: 在尾部增加一个或多个数据,返回增加后的长度,原数组改变 unshift: 在头部增加一个或多个数据,返回增加后的长度,原数组改变


  2. pop: 删除尾部第一个数据,返回删除的数据,原数组改变 shift: 删除头部第一个数据,返回删除的数据,原数组改变


  3. indexOf: 接收2个参数(value, start),从左到右进行查询,返回value的索引值,如果不存在则返回-1;start为可选值,表示开始查询的位置,默认从0开始。 lastIndexOf: 与indexOf相反,从右开始查询

  4. 截取
    slice: 接收2个参数(start,end),start必选,表示从哪里开始截取;end表示截取到哪个位置为止(不包含end),可省略,省略表示到最后一位。start和end均可为负数,表示从最后一位开始数。返回被截取的数组,原数组不变。
    splice: 接收多个参数(start,num,data1,data2...),所有参数可选。不传入参数时,不进行任何操作;只传入start,表示从start位置(包含start)开始删除到数组结尾;传入start和num,从start开始删除num个值;传入多个,从start开始删除num个值,并将后续的参数插入到start位置。返回截取的数组,原数组改变

  5. 遍历(均不会改变原数组)
    forEach: 接收的参数为回调函数,回调默认接收3个参数: val(数组数据), index(索引值), self(遍历的数组)。 没有返回值,不会改变原数组
    map: 接收参数同forEach。会将遍历所产生的返回值组成新数组并返回
    filter: 接收参数同forEach。回调函数的返回值需要接收布尔值,会将返回值为true的数据组成新数组返回
    every: 接收参数同forEach。回调函数的返回值需要接收布尔值,当返回值为true,会继续遍历;当返回值为false,则会结束遍历。只要有一个返回值为false,整个函数的返回值就为false;只有全部返回值为true,最终返回值才会为true
    some: 接收参数同forEach。回调函数的返回值需要接收布尔值。与every相反,当返回值为true,结束遍历;当返回值为false会继续遍历。只要有一个返回值为true即最终返回true,否则返回false
    reduce: 接收2个参数callback和initial(初始值,可省略),callback默认接收4个参数(pre,val,index,self)。当不传入initial时,遍历从第二项开始,此时pre的值为数组第一位,往后继续遍历时pre的值取决于callback的返回值;若传入initial,遍历从第一项开始,此时pre的值为initial。函数返回最后一次遍历时回调的返回值

  6. 其他
    sort: 排序。会改变原数组
    concat: 连接多个数组,不改变原数组,返回连接后的数组
    join: 接收一个参数(str),可选,默认为‘,’,将数组转换成字符串并以传入的参数作为间隔
    reverse: 翻转数组,会改变原数组
    toString: 类似于不传参的join函数,发生隐式类型转换时会自动调用该函数

# 5. 字符串方法

JavaScript 中字符串一旦创建就不会再改变,所有涉及改变字符串的操作都是在创建的副本上操作,所以也不会改变原字符串

  • 操作方法
  1. 增:+、concat()
  2. 删:slice(start, end)、substr(start, end)、substring(start, end)
  3. 改:
    去除空格: trim()trimLeft()trimRight()
    重复:repeat(n), n表示字符串重复次数
    填充:padStart(len, str)padEnd(len, str), 如果字符串长度小于len, 则用str在对应位置填充至len长度
    大小写转换: toUpperCase()toLowerCase()

  4. 返回指定位置的字符:charAt(i)
    返回指定字符串的位置:indexOf(str)、lastIndexOf(str)
    判断是否以某字符串开头:startsWith(str)
    判断是否包含某字符串:includes(str)
  • 转换方法
    split(str):将字符串按指定字符(串)分割成数组的每一项

  • 模板匹配方法

  1. match():接收一个正则表达式字符串或 RegExp 对象,返回数组
  2. search():接收参数同上,返回匹配的索引,找不到则返回 -1
  3. replace():接收2个参数,第一个为匹配的内容,第二个为替换的元素,返回替换后的字符串

# 6. 浅拷贝和深拷贝

只有引用类型才存在浅拷贝和深拷贝的区别。浅拷贝只复制对象的引用地址,即新老对象指向的是同一块内存;深拷贝是在堆中创建一个新的对象,使这个对象的值等于拷贝的对象的值。

  • 浅拷贝
  1. Object.assign()
  2. Array.prototype.slice()、Array.prototype.concat()
  3. 使用扩展运算符实现的复制
  • 深拷贝
  1. JSON.stringify()
    缺点:会忽略 undefined、Symbol() 和 函数
  2. 手写循环递归
function deepClone(obj, hash = new WeakMap()) {
  if (obj == null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
return cloneObj;
}

# 7. 闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

let demo = function(n){
  let num = n;
  return function(){
    num++;
    console.log(num);
  }
}

let change = demo(1);  // 返回的这个内部函数即为闭包
change() // 2
change() // 3


闭包的作用主要有2个:

  1. 延长生命周期
  2. 私有化变量

运用:柯里化函数,即是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

let fn = function(a, b, c){
  return a + b + c;
}
console.log(1, 2, 3) // 6

let curr = function(fn){
  return function(a){
    return function(b){
      return function(c){
        return fn(a, b, c);
      }
    }
  }
}
console.log(curr(fn)(1)(2)(3)) // 6

缺点:使用不当会造成内存泄漏,当一个闭包不再使用时应将其赋值为 null

# 8. EventLoop

由于 JS 是单线程的,即 JS 每一个时刻只能执行一个任务,如果前一个任务需要执行很久,后一个任务就需要等待很长时间才能够执行。而这些耗时长的任务往往是诸如网络请求等 I/O 操作,这就会导致 CPU 会有很长一段时间是空闲的。

为了解决诸如此类的阻塞问题,JS 将任务分为同步和异步两种。
1)所有同步任务都由主线程执行,形成一个执行栈;
2)而异步任务会交由 Web Api 处理,而后进行到任务队列中;
3)当所有同步任务执行完毕,执行栈为空后,就会读取任务队列的队首事件进入执行栈,由主线程执行;
4)主线程会不断重复上述过程,也就是所谓的 事件循环(Event Loop)

异步任务可以进一步分为 宏任务 和 微任务,微任务会存储到任务队列中的微任务队列,当执行栈为空后,会先判断微任务队列是否为空,不为空则先执行完微任务队列中的事件,再执行宏任务。每执行完一个宏任务,也会先判断微任务队列是否为空,不为空先执行完微任务,再执行下一个宏任务。

常见宏任务有:1. script(整体代码); 2. setTimeout/setInterval; 3. setImmediate、I/O(node.js); 4. UI事件; 5. postMessage、messageChannel
常见微任务有:1. new Promise().then()(回调函数); 2. process.nextTick(node.js); 3. Proxy; 4. MutationObserve

# 9. 作用域链

JS 作用域可分为:

  1. 全局作用域:在程序的任何位置都可访问到
  2. 函数作用域:或称为局部作用域,只在函数内部可访问到
  3. 块级作用域:用 const 或 let 定义在 {} 内的变量,只在该 {} 内可访问

JS 遵循的是词法作用域,或者可叫做静态作用域,即变量在定义时,其作用域就已经确定下来了

在 JS 使用一个变量时,会先从当前作用域下去寻找,如果没有找到,会接着从它的上级作用域寻找,以此类推直到找到该变量或已经到了全局作用域。如果到最后都没找到该变量,就会在全局作用域隐式声明该变量或直接报错

# 10. this 指向问题 !!

绑定规则:

  1. 默认绑定:全局定义函数时,函数内部的 this 会默认绑定到全局对象 window,严格模式下为 undefined
  2. 隐式绑定:对象内部定义函数时,this 会指向该对象
  3. new 绑定:通过 new 创建对象,this 指向该实例对象
  4. 显示绑定:使用 call、bind、apply 函数改变 this 的指向

Tips:this 永远指向最后调用它的对象

箭头函数

  1. 没有自己的 this,而是继承上一层作用域的 this 指向,也无法使用 call、bind、apply 改变 this 指向
var name = 'parent'
  var obj = {
    name: 'son',
    showName1: function(){
      console.log('普通函数: ' + this.name)
    },
    showName2: () => {
      console.log('箭头函数: ' + this.name)
    }
  }

  obj.showName1()   // 普通函数: son
  obj.showName2()   // 箭头函数: parent

  obj.showName2.call(obj) // 该变this指向无效,输出->箭头函数:parent
  1. 没有 arguments
  2. 不能作为构造函数,和 new 一起用会抛出错误
  var demo = () => console.log('demo'); 
  new demo()  // 报错,demo is not a constructor

# 11. arguments

概念: arguments 是用于接收函数所有参数的一个局部变量,它是一个类数组(即具有length 属性且可通过索引获取值,但没有数组的内置方法)
转成数组的方法:

  1. 使用 Array.from() 方法
  let arr = Array.from(arguments)
  1. 使用扩展运算符
  let arr = [...arguments]
  1. 使用 slice() + call/apply 实现
  let arr = Array.prototype.call(arguments)
  1. 使用 concat() + call/apply 实现
  let arr = Array.prototype.apply([], arguments)

# 11. call、apply、bind

call, apply, bind 都可用于改变 this 的指向,但这三者之间也有区别。 call 和 apply 一样接收都接收两个参数,第一个参数是 this 的指向,第二个参数是函数接收的参数,但 call 接收的参数列表,而 apply 接收的是数组形式。call 和 apply 改变 this 后会立即执行原函数,即只能临时改变 this 的指向一次;
bind 同样接收参数与 call 相同,但 bind 不会立即执行原函数,而是会返回一个改变 this 后的新函数。并且 bind 接收参数列表是可分成多次传入。

手写 bind 函数

  Function.prototype._bind1 = function(target, ...arg1){
    let self = this
    return function(...arg2){
      self.apply(target, arg1.concat(arg2))
    }
  }

  // 使用箭头函数实现
  Function.prototype._bind2 = function(target, ...arg1){
    return (...arg2) => {
      this.apply(target, arg1.concat(arg2))
    }
  }

# 12. 原型链

function check(left, right){
    // 先判断是否为基础类型,如果是,直接返回 false
    if(typeof left !== 'object' || left === null) return false;

    var sign = right.portotype;
    while(left._proto_ !== null)
    {

      if(left._proto_ === sign) return true;
      else left = left._proto_;
    }

    return false;

  }

# 13. new 操作符

function mynew(func, ...args)
  {
    const obj = {};
    obj._proto_ = func.prototype;
    let res = func.apply(obj, args);
    return res instanceof Object? res: obj;

  }

# 14. 变量提升和函数提升 (opens new window)

js 引擎在正式在正式执行前,会先将变量、函数的声明提升到其作用域的最顶端,函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序执行

var foo = 3;

function hoistFunction() {
    console.log(foo); // function foo() {}

    foo = 5;
    
    console.log(foo); // 5,由于所在作用域内存在foo,所以不会访问父作用域的foo

    function foo() {}  // 函数声明,会被提升至其所在作用域的顶部

    var foo = function(){}  // 函数表达式

    console.log(foo)  // function foo() {}
}

hoistFunction();
console.log(foo);     // 3

# 15. ajax

function myajax(option)
  {
    // 创建 xmr 对象
    const xmr = new XMLHttpRequest()

    // 初始化数据
    option = option || {}
    const type = (option.type || 'GET').toUpperCase()
    const param = option.data

    // 发送请求
    if(type === 'GET'){
      xmr.open('GET', param === null? option.url: option.url + '?' + param, true)
      xmr.send(null)
    }else{
      xmr.open('POST', option.url, true)
      xmr.send(param)
    }

    // 接收请求
    xmr.onreadystatechange = function(){
      console.log(xmr.readyState)
      if(xmr.readyState === 4){
        var status = xmr.status
        if(status >= 200 && status < 300){
          console.log('获取数据成功')
          console.log(JSON.parse(xmr.responseText))
        }else{
          console.log('连接失败')
        }
        
      }
    }

  }

  myajax({
    url: 'http://127.0.0.1:3007/article/all',
    type: 'Get',
    data: null
  })

# 16. map 和 object 的区别

  1. map的key值可以是任意数据类型,而bject的key值只能为整数、字符串、 symbol
  2. map的元素顺序遵循插入的顺序,而object则没有这个特性
  3. map支持迭代,可以用 for..of.. 遍历,而 object 不行

# 17. JS 遍历方式的不同

for..in..
for..in.. 遍历的是索引,当用此遍历数组时,会将其原型上的可遍历属性也遍历出来,一般用来遍历对象

for..of..
遍历的是属性值,只能用于有迭代器对象的集合,如数组、字符串、map、set等,不能遍历对象

forEach
不能用break打断循环,没有返回值,return也无法生效

# 18. JS 为什么是单线程

JS 作为浏览器脚本语言,其主要用途是与用户互动和操作dom,如果 JS 是多线程,当两个线程同时对同一个 dom 元素进行操作,必然会造成冲突。如果要解决这种冲突,则JS必然会变得复杂,效率也会降低,给用户的体验也会下降

# 19. JS 错误捕捉

  1. try catch finally
    只能捕捉到运行时的错误,无法捕获语法错误
  2. window.onerror 运行时错误、语法错误都能捕捉到
/*
    message:错误信息(字符串)。
    source:发生错误的脚本URL(字符串)
    lineno:发生错误的行号(数字)
    colno:发生错误的列号(数字)
    error:Error对象(对象)
    若该函数返回true,则阻止执行默认事件处理函数。
*/
window.onerror = function(message, source, lineno, colno, error) { 
    // onerror_statements
}

/*
    ErrorEvent类型的event包含有关事件和错误的所有信息。
*/
window.addEventListener('error', function(event) { 
    // onerror_statements
})

# 20. Symbol类型 (opens new window)

概念
独一无二的值,即使传入相同的参数创建出来也不相同

用途

  1. 避免对象属性被修改或覆盖
  2. 模拟私有属性或方法

# 21. 事件模型

原始事件模型(DOM 0级)

  • 事件绑定方式
let btn = document.getElementById('btn')
btn.onclick = fn
  • 解绑方式
btn.onclick = null
  • 特点
  1. 绑定速度快
  2. 只支持冒泡,不支持捕获哦
  3. 每个事件只能绑定一个监听函数,绑定多个时,后一个会覆盖前一个

标准事件模型(DOM 1级) 也是谷歌使用的事件模型,主要分为三个阶段:
1)事件捕获阶段:事件从 document 一直传到目标节点,期间经过的元素节点如果有监听函数,则触发
2)事件处理阶段:事件到达目标节点,触发回调函数
3)事件冒泡阶段:事件从目标节点冒泡到 document, 期间检测经过的元素是否有监听函数,如果有则触发

  • 绑定方法
addEventListener(EventType, callback, useCapture)
// 例子
btn.addEventListener('click', fn)
  • 解绑方法
removeEventListener(EventType, callback, useCapture)
// 例子
btn.removeEventListener('click', fn)
  • 特点
  1. 事件类型不用 + on
  2. 可通过设置 useCapture 表明是否在捕获阶段进行事件处理,默认为 false,即在冒泡时进行处理
  3. 同一个事件类型可添加多个回调函数

IE 事件模型
IE 事件模型只有事件处理和冒泡两个阶段

  • 绑定
attachEvent(eventType, callback)
  • 解绑
detachEvent(eventType, callback)
  • 实例
let btn = document.getElementById('btn')
btn.attach('onclick', fn)
btn.detach('onclick', fn)

阻止冒泡和默认行为

  • 冒泡
event.stopPropagation()  // 不考虑兼容性

event.cancelBubble = false // IE9 以下版本
  • 默认行为 点击a标签会跳转到对应页面、点击输入框获取焦点等这些属于默认行为
event.preventDefault()  // 不考虑兼容性

window.event.returnValue = false  // IE 低版本