JS基础总结(持续更新中)

321 阅读51分钟

浏览器原理

对于我们前端来说,可以简单的把浏览器理解为是由渲染引擎+JS引擎组成.

渲染引擎的作用包含解析html(触发资源请求,生成dom),生成render树,计算页面布局,绘制,以及样式改变下的重排(对布局位置重新计算),重绘(绘制在屏幕上)渲染引擎与JS引擎为互斥关系,但根据timeline发现,JS执行时布局、重排和解析html(不包括dom生成)也可能会同时执行,但绘制、重绘与JS一定是互斥的

这也许是浏览器做的优化策略,在JS引擎执行时,渲染引擎也不会完全不工作,而会做一些计算布局及解析html的事情,总之,浏览器在尽可能快的加载页面**

当前主流渲染引擎内核:

  1. firefox使用gecko引擎
  2. IE使用Trident引擎
  3. 2015年微软推出自己新的浏览器,原名叫斯巴达,后改名edge,使用edge引擎
  4. opera最早使用Presto引擎,后来弃用
  5. chrome\safari\opera使用webkit引擎
  6. 13年chrome和opera开始使用Blink引擎

JS引擎:解析、执行JavaScript代码

当前知名的JS引擎

  1. IE(Edge):JScript(IE3 - - IE8)/ Chakra(查克拉 IE9之后)
  2. Chrome:大名鼎鼎的 V8 引擎
  3. Firefox:SpiderMonkey(1.0-3.0)/ TraceMonkey(3.5-3.6)/ JaegerMonkey(4.0-)
  4. Opera:Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-)
  5. Safari:Nitro(4-)

V8引擎工作流程

V8工作流程官方定义:Blink将源码交给V8引擎->Stream(词法分析)获取源码并进行编码转换成tokens->tokens经过PreParser(预解析)和Parser(语法分析器)转换成AST抽象语法数->AST树被Ignition(解释器/转换器)转成字节码 加入自我理解的V8引擎工作流程:JS源代码->解析(词法分析/语法分析)->AST(抽象语法树)->ignition(解释器/转换器)->字节码(为了适配不同环境的CPU架构)->机器代码 或者 TurboFan(编译器 将字节码编译为CPU直接执行的机器码)->运行结果

TurboFan的作用是用于优化执行效率,假如一个函数被多次调用就会被标记,之后就会被转换成优化的机器码,提高性能.如果在后续执行函数过程中,类型发生了变化等,它会将机器码逆向转回字节码 image.png

JavaScript的执行过程

  • JS引擎在执行代码前,会在堆内存中创建一个全局对象Global Object,该对象所有的作用域scope任意地方都可以对它进行访问,它里面会包括Date、Array等内置方法或属性等等,其中还有一个window指向自己.感兴趣的朋友可以在F12 控制台打印window.window.window尝试.
  • JS引擎内部有一个执行上下文栈(Execution content stack,简称ECS),他是用于执行代码的调用栈,他首先会执行的是全局的代码块;
  • 全局代码块执行会构建一个Global Execution content(GEC),GEC会被放入到ECS中执行,GEC被放入到ECS中里面包含俩部分内容:
  1. 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量,函数等加入到GlobalObject中,但并不会赋值;这个过程也称之为变量的作用域提升.
  2. 第二部分:在代码执行中,对变量赋值,或者执行其他的函数
  • 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Function Execution Content,简称FEC),并且压入到EC Stack中.
  • FEC中包含三部分内容:
  1. 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO);AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
  2. 第二部分:作用域链:由VO (在函数中就是AO)和父级VO组成,查找时会一层层向上查找;
  3. 第三部分:this绑定的值

JS的内存管理

JavaScript会在定义变量时为我们分配内存.JS对于基本数据类型会直接在栈空间进行分配;JS对于复杂数据类型会在堆内存中开辟一块空间,并且将这块空间的指针返回值给变量引用

JS的垃圾回收

我们都知道内存的大小是有限的,所以当变量不再需要的时候,我们需要对其内存进行释放,以便腾出更多的空间. 垃圾回收的英文Garbage Collection,简称GC;垃圾回收器我们也会简称为GC;GC怎么知道哪些对象是不再使用的呢?这就涉及到我们常见的GC算法

  • 引用计数:当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;这个算法有一个很大的弊端就是会产生循环引用
let obj1={
    a:obj2
}
let obj2={
    a:obj1
}  
  • 标记清除:这个算法是设置一个根对象(root object),垃圾回收期会定期从这个根开始,找所有从跟开始有引用到的对象,对于没有引用到的对象就认为是不可用的对象 JS引擎就是采用的标记清除算法,当然类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法

作用域

  • 在ES5中作用域分为全局作用域和函数作用域
  • 而当函数参数有默认值时会形成一个新的参数作用域.所以函数作用域分为参数作用域和函数内作用域

闭包

在MDN中的定义一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;

1.闭包的内存泄漏

  • 假设执行以下代码:
function foo(count) {
 return function(num) {
   return num + count
 }
}
var ming=foo(7)
console.log(3)

首先我们知道JS的垃圾回收采用的方法是标记清除法(上一小点中已经提到),首先当执行到闭包代码时,由于ming会等于foo函数返回的匿名函数,而这个匿名函数引用着其父作 用域foo中的自由变量,所以这样一层层的引用关系导致引用连中的所有对象都是无法释放的,这样就形成了内存泄漏.

this

this绑定的方式

  • 默认绑定 //foo() 独立的函数调用 this指向window
  • 隐示绑定 //obj.foo() this指向obj
  • 显示绑定 //obj.foo.call(obj1) this指向obj1
  • new绑定 //var a = new Foo(); this指向调用这个Foo构造器时创建出来的a对象

this优先级规则

  1. 默认绑定
  2. 显示绑定优先级高于隐式绑定
  3. new绑定优先级高于隐式绑定
  4. new绑定优先级高于bind

this绑定规则

  1. 箭头函数没有自己的this 4种this绑定的方式都无效,箭头函数内部的this 永远指向上层作用域
  2. 函数不管在任何地方调用,只要是独立函数调用,this一定指向window.什么是独立函数调用呢?最简单的就是只有函数名+括号 比如:foo()或者bar()再或者(fn)()
  3. 在隐式绑定规则下且无其他绑定规则干扰,XX.foo() 函数的this 永远指向调用它的XX
      var object ={}
      var obj1 = {
        name: "obj1",
        foo: function () {
          console.log(this)
        },
        foo1: function () {
            return function (){
                console.log(this)
            }
        },
        foo2: function () {
            return () => {
                console.log(this)
            }
        }
      };
      var obj2 = {
        name: "obj2",
      }; //这里一定要加个分号表示结束,否则JS引擎在解析的时候 会把下面括号()的语法跟这里算做同一块的代码,结果就是造成异常
      (obj2.bar = obj1.foo)(); //window  这里有个赋值表达式 所以是间接函数引用,属于一个独立得函数调用,打印的是window对象
      obj1.foo1()() //window 这里许多同学会搞错,以为是隐式绑定.NO,这么想你就错了,因为这个代码我们是这么执行的 先执行obj.foo1()拿到返回的函数后再执行.所以obj隐式绑定的是foo1并不是之后执行的匿名函数.所以这里会是一个独立的函数调用,this指向window   (obj1.foo1())()
      obj1.foo1.call(object)() //this还是指向window,同理修改的是foo1的this 并没有修改最后执行的函数.
      obj1.foo1().call(object) //this指向object
      obj1.foo2()() //obj1 箭头函数没有this,永远指向上层作用域  在这里上层作用域是obj1.foo2()调用产生的,所以this指同foo2一样一起指向obj1
      obj1.foo2.call(object)() //object 上层作用域被显示绑定修改为object 所以这里this指向object
      obj1.foo2().call(object) //obj1 箭头函数永远指向上层作用域,所以这里指向obj1
var name = 'window'

function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

person1.obj.foo2()() // obj  箭头函数永远指向上层作用域 上层作用域是foo2 foo2函数是被obj调用的,所以this同foo2一起指向obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj

this实战 实现call apply bind

JS的函数

  1. JS除了箭头函数外 所有定义的函数中都会有个arguments,它里面包括了所有接收到的参数和一个callee方法 这个方法就是函数本身.
  2. arguments 是一个类数组 他不是真正的数组 它并不具备数组Array原型中的方法 比如ForEach等等,但是它可以通过下标访问,并且有一个length属性表示长度
//arguments 大概是这个样子
arguments = {
    1:'a',
    2:'b',
    3:'c',
    length:3,
    caller:function (){/*这个函数就是当前函数本身*/}
}

//自执行函数
(function (a,b){
    console.log(a+b)
})(1,2)

JS函数式编程?我对函数式编程的理解和看法

被淘汰的语法 这辈子不要再用!!!

//with用法
const obj = {
  name: "luo",
  age: 18,
}
const name = 1
const age = 2
with (obj) {
  console.log(name) //luo
  console.log(age)  //18
}

//eval用法
var x = 5
var y = 5
var geval = eval
console.log(geval("var x=1, y=1; x + y")) // 2
console.log("1", x, y)  // 5,5

console.log(eval("var x=1, y=1; x + y")) // 2
console.log("2", x, y)  // 1,1


let a = 5
let b = 5
console.log(eval("let a=1, b=1; x + y")) // 2
console.log(a, b)  // 5,5

// 绝佳的替代eval方案
var x = 5
var y = 5
console.log(Function('"use strict";var x=1, y=1; x + y')) // [Function: anonymous]
console.log(x, y)  // 5, 5

JS严格模式

JS严格模式会将正常模式下一些过时错误认为是异常

  1. 无法意外创建全局变量 // a = '1'
  2. 严格模式会将正常模式下的过时错误认为是异常
  3. 严格模式下试图删除不可删除的属性 // delete Object.prototype; // 抛出TypeError错误
  4. 严格模式不允许函数的参数有相同的名称
  5. 严格模式不允许0的八进制语法
  6. 严格模式下不允许使用with
  7. 严格模式下,eval不再为上层引用新变量
  8. 严格模式下,this绑定不会默认转成对象

深入面向对象

JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:

  1. JS中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成;
  2. key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;
  3. 如果值是一个函数,那么称为对象的方法;

如何创建对象

// 方式一
let obj1 = new Object()
obj1.name = "luo"
obj1.foo = function() {
    console.log(1)
}

// 方式二
let obj2 = {
    name:"name",
    foo(){
        console.log(2)
    }
}

属性描述符

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符Object.defineProperty. Object.defineProperty(obj,prop,descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象.

属性描述符分类

  1. 数据属性描述符
  2. 存取属性描述符
configurableenumerablevaluewritablegetset
描述表示是否可以delete或者修改它的特性以及修改为存取属性描述符表示是否可以通过for-in或者Object.keys()返回该属性属性的value值是否可以修改属性值获取属性会执行的函数设置属性会执行的函数
默认值直接创建对象为true,用属性描述符定义时为false直接创建对象为true,用属性描述符定义时为falseundefined直接创建对象为true,用属性描述符定义时为falseundefinedunderfined
数据描述符可以可以可以可以不可以不可以
存取描述符可以可以不可以不可以可以可以

同时定义多个属性

Object.defineProperties() 方法直接在一个对象上定义多个新属性或者修改现有属性

let obj = {
   _age:18
}
Object.defineProperties(obj, {
    name:{
        writable:true,
        value:why
    },
    age:{
        get:function(){
            return this._age
        }
    }
})

对象方法补充

  1. 获取对象的属性描述符
    • Object.getOwnPropertyDescriptor(obj,prop)
    • Object.getOwnPropertyDescriptors(obj)
  2. 禁止对象扩展新属性
    效果:给一个对象添加新的属性会失败(在严格模式下会报错)
    • Object.preventExtensions(obj)
  3. 密封对象,不允许配置和删除属性 但是如果原来是writable可写的,依旧可写
    原理:实际是调用preventExtensions并且将现有属性configurable:false
    • Object.seal(obj)
  4. 冻结对象,不允许修改现有属性
    原理:实际上是调用seal,并且将现有属性的writable:false
    • Object.freeze()
  5. 对象自身属性中是否具有指定的属性
    • obj.hasOwnProperty('name')
  6. 判断某个属性是否在某个对象或者对象的原型上
    • 'name' in obj
  7. 检测构造函数的prototype是否出现在某个实力对象的原型链上
    • arr instanceof Array
  8. 检测某个对象是否出现在某个实例对象的原型链上
    • 实例对象.isPrototypeOf(某个对象)

创建对象方案-工厂模式

function Person(name,age){
    let p = new Object()
    p.name = name
    p.age = age
    return p
}
let p1 = Person("luo",18)

工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型

1.png

认识构造函数

  1. 构造函数也称为构造器(constructor),通常是我们在创建对象时会调用的函数;
  2. JS中构造函数也是一个普通的函数,从表现形式来讲和普通函数没有区别;
  3. 如果一个普通的函数被使用new操作符来调用了,那么这个函数就称为一个构造函数;

那么new操作符有什么特殊呢? 如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行函数的内部代码(函数体代码);
  5. 如果构造函数没有返回对象或者数组,则默认返回创建出来的新对象
  function Person(name,age){
    this.name = name
    this.age = age
  }
  let p1 = new Person("luo",18)
  console.log('p1',p1);

2.png 如图构造函数可以确保我们的对象是有createPerson的类型的(实际是constructor的属性)

构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例,如果构造函数中有定义方法,每次创建不同对象都会创建出重复的函数

JS当中每个对象都有一个特殊的内置属性 称为隐式原型[[prototype]],这个特殊的属性可以指向另外一个对象. 那么这个属性有什么用呢?

  • 当我们通过引用对象的属性key来获取一个value时,它会触发[[Get]]的操作;
  • 这个操作会首先检查该引用对象是否有对应的属性,如果有的话就使用它;
  • 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;

那么如果通过字面量直接创建一个对象,该怎么获取这个属性呢?

  1. 通过对象的__proto__属性可以获取到(这个属性是浏览器自己添加的,并非ECMA所规定所以可能存在兼容性问题)
  2. 通过 Object.getPrototypeOf(obj) 方法获取

函数的显示原型 prototype

从上文我们得知new操作 会把内存中创建新对象的隐式原型[[prototype]]指向构造函数的显示原型prototype,所以说明有且只有函数才都会有这个prototype属性

  function Person(name,age){
    this.name = name
    this.age = age
  }
  let p1 = new Person("luo",18)
  let p2 = {}, p3 = {}
  p2.__proto__ = Person.prototype
  p3.__proto__ = Person.prototype
  console.log(p1.__proto__ === Person.prototype); //true
  console.log(p1.__proto__ == p2.__proto__); //true
  console.log(p2.__proto__ === p3.__proto__); //true

原型对象的constructor

  • 默认情况下原型对象上是有一个属性的:constructor ,它指向当前函数对象
  • 每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获取constructor属性 我们虽然可以重写Person的prototype属性,但会造成默认constructor属性不可枚举变成了可枚举, 如果要解决这个问题,我们可以使用以下的方法
function Person(name,age){
    this.name = name
    this.age = age
}
Person.prototype={
    constructor:Person,
    a:1
}
Object.defineProperty(Person.prototype,'constructor',{
  enumerable:false,
  value:Person
})

每个函数都会有个原型对象prototype,函数原型对象中的constructor又会等于函数本身

  //伪代码
  Person():{
      prototype:{
        constructor:Person()
      }
  }

  function Person(name,age){
    this.name = name
    this.age = age
  }
  let p1 = new Person('luo',12)
  console.log(Person === Person.prototype.constructor); //true
  console.log(p1.__proto__ === Person.prototype); //true

创建对象 - 构造函数和原型结合

之前了解构造函数创建对象时其中一个弊端 就是每个新对象都会创建出相同重复的函数. 用下面的方法就可以让所有对象去共享同一个函数了

  function Person(name,age){
    this.name = name
    this.age = age
  }
  Person.prototype.eating = function() {
    console.log('eating')
  }
  let p1 = new Person('luo',12)

JS中面向对象的特性

var/let/const 作用域提升

console.log(foo) //underfined
var foo = "foo"

console.log(bar) 
// Uncaught ReferenceError: Cannot access 'bar' before initialization
//引用错误:不能在bar初始化之前访问
let bar = "bar"
var foo = "foo"
if(true){  
    console.log(foo) 
    // Uncaught ReferenceError: Cannot access 'foo' before initialization
    //引用错误:不能在bar初始化之前访问
    let foo = "bar"
}

function foo(m = n + 1, n){
 // Uncaught ReferenceError: Cannot access 'n' before initialization
 //引用错误:不能在bar初始化之前访问
console.log(m)
}
foo()
整个{ }内的区域称为暂时性死区:使用letconst声明的变量,在声明之前,变量都是不可以访问的;

从上述代码可以看出let声明的变量,在声明之前访问会报错,但并不是在代码执行阶段才会创建 ECMA262规范里规定:let const声明的变量会被创建在包含他们的词法环境被实例化时,但是不可以访问他们,直到词法绑定被求值。
个人总结:我在官方资料中并没有看到对作用域提升这个概念的解释,所以只能从字面量上理解,我认为一个变量如果在声明前就可以访问,才能称之为作用域提升,在let/const中 虽然会在解析阶段被创建出来,但无法提前访问 所以我个人认为let、const没有作用域提升。

var/let/const 与window的关系

VE.png

  • 从上图ECMA规范中得知我们声明的变量和环境记录是被添加到变量环境中的
  • 标准中没有规定这个对象是window对象或者是其他对象,每个JS引擎在解析的时候都会有自己的实现
  • 例如V8中其实是通过variableMap的一个hashmap来实现它们的存储的。
  • window对象是早期的go对象,在最新的实现中其实是浏览器添加的全部对象,并且一直保持window和var之间值的相等性
    个人总结:所以let/const跟window没有任何关系,var和window绑定是浏览器实现的

标签模板字符串

const name = "luo"
const age = 18
fn`hello${name}hi${age}`
function fn(arg1,...args){
console.log(arg1) // ['hello','hi','']
console.log(args) // ['luo',18]
}

函数默认值/函数的剩余参数

默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了

function fn(a,b,c=9,d,e) {
console.log(fn.length) //2
}
fn(1,2,3,4,5)

function bar(a,...args){
//剩余参数...args必须放到最后一个位置,否则会报错
}

数值表示

//ES6

const num1 = 100 // 10进制
const num2 = 0b10 // 2进制
const num3 = 0o100 // 八进制
const num4 = 0x100 //16进制

//ES12
const num5 = 100_000_000  // 可以使用连接符_

Symbol

Symbol可以生成一个独一无二的值

    let a = Symbol('1')
    let b = Symbol('1')
    a === b //false

    let obj = {}
    let a = Symbol("a")
    let b = Symbol("a")
    obj[a] = 1
    //ES10新增
    Object.defineProperty(obj, b, {
      enumerable: true,
      configurable: true,
      writable: true,
      value: 12,
    })
    for (const key in Object.getOwnPropertySymbols(obj)) {
      console.log("key", key)
    }

如果想创建相同的Symbol

// 可以使用Symbol.for方法来
 let a = Symbol.for('1')
 let b = Symbol.for('1')
 a === b //true

// 通过Symbol.keyFor方法来获取对应的key
let a = Symbol("a")
let c = Symbol.for('a')
console.log(Symbol.keyFor(a)) //undefined
console.log(Symbol.keyFor(c)); //a

Set

Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复

Set常用属性与方法
size返回Set元素的个数
add()添加某个元素,返回Set对象本身
delete()从Set中删除和这个值相等的元素,返回Boolean类型
has()判断Set中是否存在某个元素,返回Boolean类型
clear()清空set中所有的元素,没有返回值
forEach()遍历整个Set
const set1 = new Set()
set1.add(1)
set1.add(2)
set1.add(3)
console.log(set1); //Set(3) {1, 2, 3}

WeakSet

  1. WeakSet不能遍历,因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁,所以存储到WeakSet中的对象是没办法获取的。
  2. WeakSet 只能存放对象类型
  3. WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收 那么WeakSet有什么用呢?事实上我们可能很少能够用到它,引用一个Stack Overflow的答案

ws.png

Map

map用于存储映射关系,那么它与对象又有什么区别呢?

ObjectMap
用法1. 键的类型:只能是字符串/Symbol
2.顺序:不会完全保持插入时的顺序
3. 计算长度相对复杂:Object.keys(obj).length
4. 默认不可迭代,只支持for-in访问(也可以使用对象方法访问键/值,例如:Object.keys(o)、Object.values(o)、Object.entries(o)、obj[Symbol.iterator] !== undefined; // false
5. 可以覆盖原型上的键
6. JSON默认支持Object
1.键大类型:任何类型;
2. 保持其插入时的顺序;
3. 计算长度简单:map。size()
4. 可迭代对象,支持for-of、forEach、map[Symbol.iterator]!==undefined // true
5. 不会覆盖原型上的键
6. JSON默认不支持Map
句法1.创建
const o = {}; // 对象字面量
const o = new Object(); //调用构造函数
const o = Object.create(null) //调用静态方法
2. 新增/修改元素 o.x = 1 //属性名不能包括空格、标点符号、不能以数字开头
o['y'] = 2
3. 读取元素
o.x或者o?.['y']
4. 删除元素 delete o.b
1.创建:const map =new Map() //调用构造函数
2. 新增/修改元素 map.set('x', 1)
3.读取元素
map.get('x')
4. 删除元素 map.delete('b')
性能Map性能比Object好 (占用内存小,增删速度更快)
适合场景1.只是简单的数据结构时,选择Object,因为它在数据少的时候占用内存更少,且新建时更为高效
2. 需要用到JSON进行文件传输时,选择Object因为JSON不默认支持Map
3. 需要对多个键值进行运算时,选择Object,因为语法更为简洁
4. 需要覆盖原型上的键时,选择Object
1. 存储的键不是字符串/数字/Symbol时选择Map,因为Object不支持
2. 存储大量的数据时,选择Map因为它的占用内存更小
3. 需要进行许多新增/删除元素的操作时选择Map,因为速度更快
4. 需要保持插入时的顺序的话,选择Map,因为Object会改变排序 5. 需要迭代/遍历的话选择Map,因为它默认是可迭代对象,迭代更便捷

Object中各属性排序规则

  • 非负整数 会最先被列出,排序是从小到大的数字顺序
  • 然后所有字符串,负整数,浮点数会被列出,顺序根据插入的顺序
  • 最后才会列出Symbol,Symbol根据插入的顺序进行排序

JSON默认不支持Map,但是可以转一层

若想要通过JSON传输Map则需要使用到.toJSON()方法,然后在JSON.parse()中传入复原函数来将其复原。

WeakMap

WeakMap和Map有什么区别?

  1. WeakMap的key只能使用对象,不接受其他的类型作为key;
  2. WeakMap的key对对象的引用是弱引用,如果没有其他变量也在引用这个对象,那么GC可以回收该对象

WeakMap常见的方法

1.set(key,value) 在WeakMap添加key,value并且返回整个Map对象;

2.get(key) 根据key获取Map中的value;

3.has(key) 判断是否包括某一个key,返回Boolean类型;

4.delete(key)根据key删除一个键值对,返回Boolean类型;

WeakMap的应用

vue3响应式

ES7

  1. array.includes
  • array.includes(valueTofind[,fromIndex]) 返回Boolean
  • 在ES7之前如果要判断数组是否包含某个元素,需要通过indexOf获取结果,并判断是否为-1.
  1. exponentiation 指数运算符
  • const result1 = 3**3
  • 在ES7之前计算数字的乘方需要通过Math.pow(base,exponent)方法来完成

ES8

  1. Object.values
const obj = {
    name:"luo",
    age:18
}
console.log(Object.values(obj)) // ['luo', 18]
console.log(Object.values('abc')) // ['a', 'b', 'c']
  1. Object.entries 通过Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组
const obj = {
    name:"luo",
    age:18
}
console.log(Object.entries(obj)) // [['name','luo'],['age',18]]
for (const [key,value] of Object.entries(obj)){
    console.log(key,value)
}
console.log(Object.entries(["abc","cba","nba"])) //[['0','abc'],['1','cba'],['2','nba']]
console.log(Object.entries('abc'))//[['0','a'],['1','b'],['2','c']]
  1. String padStart(targetLength,padString) padEnd(targetLength,padString) 字符串前后填充
const message = "Hello"
console.log(message.padStart(7,'a')) // aaHello
console.log(message.padEnd(7,'a')) // Helloaa
  1. Trailing Comma ES8允许在函数定义和调用时多加一个逗号
function foo(a,){
}
foo(10,)
  1. Object.getOwnPropertyDescriptors(obj) 获取一个对象的所有自身属性的描述符
let obj = {}
Object.getOwnPropertyDescriptors(obj)

ES9

  1. Async iterators
  2. Object spread operators 对象展开运算符
let obj = {
    name:'luo'
}
let obj1 = {...obj}

3.Promise.finally

方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。

ES10

  1. array.flat([depth]) 默认为1,方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
const arr1 = [0, 1, 2, [3, 4]];

console.log(arr1.flat());
// expected output: [0, 1, 2, 3, 4]
  1. array.flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组.
  • flatMap 先进行map操作,再做flat的操作;
  • flatMap 中的flat相当于深度为1
  1. Object.formEntries(iterable) 我们可以通过 Object.entries 将一个对象转换成 entries, 也可以通过Object.formEntries转成object
    const params = 'name=luo&age=18&height=1.88'
    const searchParams = new URLSearchParams(params)
    for(const param of searchParams){
        console.log(param)
    }
    const searchObj = Object.fromEntries(searchParams)
    console.log(searchObj)
  1. string.trimStart/string.trimEnd
const message = "  hello world  "
console.log(message.trim())
console.log(message.trimStart())
console.log(message.trimEnd())
  1. Symbol.description
const name = Symbol('es')
console.log(name.toString()) // Symbol(es)
console.log(name) // Symbol(es)
console.log(name === 'Symbol(es)') // false
console.log(name.toString() === 'Symbol(es)') // true

console.log(name.description)
  1. Optional Catch Binding
// old
try {
  // some code
  return true;
} catch(unusedException) {  // here is the problem
  return false;
}


// now
try {
  // some code
  return true;
} catch {
  return false;
}

ES11

  1. BigInt 在早期的JS中,我们不能正确的表示过大的数字,大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的.
const maxInt = Number.MAX_SAFE_INTEGER
console.log(maxInt)
console.log(maxInt + 1)
console.log(maxInt + 2)

引入了新的类型BigInt后

const bigInt = 9007199254740991n
console.log(bigInt + 1n)
console.log(bigInt + 2n)
  1. Nullish Coalescing Operator
console.log(""||'123')
console.log(""??'123')
  1. Optional Chaining
const obj = {
    a:{
        b:{
        }
    }
}
console.log(obj?.a?.c?.c)
console.log(obj.a.c.c)
  1. Global This
//新增了一个全局对象globalThis 在node和浏览器都可以使用
console.log(globalThis)
  1. for in标准化 在之前虽然很多浏览器支持forin遍历对象类型,但是没有ECMA标准化, E11中,对其进行了标准化,规定了用于遍历对象的key

  2. Dynamic Import

//动态引入,我们的vue 路由懒加载就有其的应用2
const routes = [
  {
    path: '/upload',
    name: 'Upload',
    component: () => import(/* webpackChunkName: "upload" */'../views/Upload.vue')
  }
]

  1. Promise.allSettled 方法返回一个在所有给定的promise都已经fulfilledrejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。
Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
  { status: 'fulfilled', value:  'a' },
  { status: 'rejected',  reason: 'b' },
]));

  1. import meta import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。
const url = import.meta.url;
export default url;

ES12

  1. FinalizationRegistry FinalizationRegistry对象可以让你在对象被垃圾回收时请求一个回调. 我们可以通过调用register注册任何我们想要清理回调的对象,传入该对象和所含的值
    let obj = { name: 'luo' }
    const registry = new FinalizationRegistry(value => {
        console.log("销毁", value)
    })
    registry.register(obj,"haha")
    obj = null
  1. WeakRef 如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用, 如果我们希望是一个弱引用的话,可以使用WeakRef
let obj = { name: "luo" }
let info = new WeakRef(obj)
console.log(info.deref().name)
console.log(obj === info.deref())

  1. logical assignment operators
let str =""
message ||="hello"
//等价于
message = message||"hello"

message ??="默认值"
//等价于
message = message ?? "默认值"

let obj = {
    name: "luo"
}
obj &&=obj.name
//等价于
obj = obj&&obj.name

  1. Numeric Separator
const num1 = 123_456_789
const num2 = 123456789
console.log(num1 === num2)
  1. String.replaceAll
const p = 'dog 1 dog 2';
console.log(p.replaceAll('dog', 'monkey'));
const regex = /Dog/ig;
console.log(p.replaceAll(regex, 'ferret'));

迭代器

迭代器是是确使用户可在容器对象(container,比如链表数组)上遍访的对象,使用该接口无需关心对象内部实现细节.

迭代器就是帮助我们对某个数据结构进行遍历的对象。

迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol)

  1. 在js中这个标准就是一个特定的next方法,返回一个对象.
  • done:如果迭代器可以产生序列中的下一个值,则为 false,如果迭代器已将序列迭代完毕,则为 true。
  • value:迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

什么又是可迭代对象呢?

  • 它和迭代器是不同的概念;当一个对象实现了iterable protocol协议时,它就是一个可迭代对象;
  • 这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性

可迭代对象的应用

JS语法:for ...of、展开语法(spread syntax)、yield*(后面讲)、解构赋值

创建对象:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])

方法调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)

迭代器的中断

迭代器在某些情况下会在没有完全迭代的情况下中断:

  1. 遍历过程中通过break/continue/return/throw中断了循环操作;
  2. 在解构的时候,没有解构所有的值; 那么这个时候我们想要监听中断的话,可以添加return方法

image.png

生成器

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执 行等。生成器事实上是一种特殊的迭代器;

生成器函数也是一个函数,但是和普通的函数有一些区别:

  • 生成器函数需要在function的后面加一个符号:*
  • 生成器函数可以通过yield关键字来控制函数的执行流程
  • 生成器函数的返回值是一个Generator(生成器)

生成器传递参数

我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值

function* foo() {
  console.log('函数开始执行');
  const value1= 100
  const value2 = yield value1
  const value3 = yield value2
  yield value3
}
const generator = foo()
console.log('generator',generator.next()); //{value: 100, done: false}
console.log('generator',generator.next(2)); //{value: 2, done: false}
console.log('generator',generator.next(3)); //{value: 3, done: false}
console.log('generator',generator.next(4)); //{value: undefined, done: true}

生成器提前结束 - return函数

function* foo() {
  console.log('函数开始执行');
  const value1= 100
  const value2 = yield value1
  const value3 = yield value2
  yield value3
}
const generator = foo()
console.log('generator',generator.next()); //{value: 100, done: false}
console.log('generator',generator.return(2)); //{value: 2, done: false}
console.log('generator',generator.next(3)); //{value: undefined, done: true}
console.log('generator',generator.next(4)); //{value: undefined, done: true}

除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常: p抛出异常后我们可以在生成器函数中捕获异常;然后继续使用yield继续中断函数的执行;

image.png

生成器替代迭代器

还可以使用yield*来生产一个可迭代对象:这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值

function* Iterator(arr) {
    yield* arr
}

自定义类迭代 – 生成器实现

image.png

异步处理方案-Generator方案

  function request(url) {
    return new Promise(resolve => {
      setTimeout(() => {
        console.log('url', url);
        resolve(url)
      }, 1500);
    })
  }

  function* getData() {
    const res1 = yield request('luo')
    const res2 = yield request(res1 + '1')
    const res3 = yield request(res2 + '2')
    const res4 = yield request(res3 + '3')
  }
  const generator = getData()
  generator.next('初始值').value.then(res => {
    generator.next('a').value.then(res => {
      generator.next('b').value.then(res => {
        generator.next('ha')
      })
    })
  })

目前我们的写法有两个问题

  1. 我们不能确定到底需要调用几层的Promise关系
  2. 如果还有其他需要这样执行的函数,我们应该如何操作呢? 所以,我们可以封装一个工具函数execGenerator自动执行生成器函数
  function request(url) {
    return new Promise(resolve => {
      setTimeout(() => {
        console.log('url', url);
        resolve(url)
      }, 1500);
    })
  }

  function* getData() {
    const res1 = yield request('luo')
    const res2 = yield request(res1 + '1')
    const res3 = yield request(res2 + '2')
    const res4 = yield request(res3 + '3')
  }
  
  function execGenerator(genFn) {
    const generator = genFn()
    function exec(res) {
      const result = generator.next(res)
      if(result.done) return result.value
      result.value.then(res=>{
        exec(res)
      })
    }
    exec()
  }

  execGenerator(getData)

异步函数 async function

异步函数的执行流程

异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行

异步函数有返回值时,和普通函数会有区别:

  1. 异步函数也可以有返回值,但是异步函数的返回值会被包裹到Promise.resolve中;
  2. 如果我们的异步函数的返回值是Promise,Promise.resolve的状态会由Promise决定;
  3. 如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;
  4. 如果我们在async中抛出了异常并且写了.catch方法,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;

await 关键字

async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的 await特点

  1. 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise
  2. 那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;
  3. 如果await后面是一个普通的值,那么会直接返回这个值
  4. 如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;
  5. 如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的 reject值

操作系统 – 进程 – 线程

操作系统的工作方式

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

  1. 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换
  2. 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码
  3. 对于用户来说是感受不到这种快速的切换的

浏览器中的JavaScript线程

我们经常会说JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。

  • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页 面卡死而造成所有页面无法响应,整个浏览器需要强制退出

  • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程

  • avaScript的代码执行是在一个单独的线程中执行的
    这就意味着JavaScript的代码,在同一个时刻只能做一件事
    如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;

  • 所以真正耗时的操作,实际上并不是由JavaScript线程在执行的

  • 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可

浏览器/Node 的事件循环

浅谈个人对JavaScript Event Loop的理解

Error类型

JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象

Error包含三个属性:

  • messsage:创建Error对象时传入的message
  • name:Error的名称,通常和类的名称一致
  • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack

Error子类

  1. RangeError:下标值越界时使用的错误类型;
  2. SyntaxError:解析语法错误时使用的错误类型;
  3. TypeError:出现类型错误时,使用的错误类型;

什么是模块化?

  • 事实上模块化开发最终的目的是将程序划分成一个个小的结构
  • 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构
  • 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
  • 也可以通过某种方式,导入另外结构中的变量、函数、对象等;

js模块化的历史

Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的
随着前端和JavaScript的快速发展,特别是AJAX/SPA的出现JavaScript代码变得越来越复杂了
所以,模块化已经是JavaScript一个非常迫切的需求:但是JavaScript本身,直到ES6(2015)才推出了自己的模块化方案,在此之前,为了让JavaScript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、CommonJS等;

没有模块化带来的问题

在早期,我们为了解决命名冲突问题,我们使用立即执行函数(IIFE)来解决 但是这样带来了新的问题

  1. 我们必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
  2. 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写; 3.在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况

所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的

  1. 我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码
  2. 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性;

CommonJS规范和Node关系

我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。

  • Node是CommonJS在服务器端一个具有代表性的实现;
  • Browserify是CommonJS在浏览器中的一种实现;
  • webpack打包工具具备对CommonJS的支持和转换;

Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发

  • 在Node中每一个js文件都是一个单独的模块
  • 这个模块中包括CommonJS规范的核心变量:exports、module.exports、require.我们可以使用这些变量来方便的进行模块化开发;

exports导出

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;

// bar.js
exports.name = name
exports.age = age
exports.fn = fn

另外一个文件中可以导入

// manin.js
const bar = require('./bar')

module.exports

module.exports和exports有什么关系或者区别呢?

  • CommonJS中是没有module.exports的概念的.Node为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module;
  • 所以在Node中真正用于导出的其实根本不是exports,而是module.exports,因为module才是导出的真正实现者 为什么exports也可以导出呢?

这是因为module对象的exports属性是exports对象的一个引用;也就是说 module.exports = exports = main中的bar;

require细节

require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象
require的查找规则是怎么样的呢? 具体规则

总结比较常见的查找规则

require(X)

情况一:X是一个Node核心模块,比如path、http;直接返回核心模块,并且停止查找
情况二:X是以 ./ 或 ../ 或 /(根目录)开头的

  1. 将X当做一个文件在对应的目录下查找
  • 如果有后缀名,按照后缀名的格式查找对应的文件
  • 如果没有后缀名,会按照如下顺序
    • 直接查找文件X
    • 查找X.js文件
    • 查找X.json文件
    • 查找X.node文件
  1. 没有找到对应的文件,将X作为一个目录
  • 查找目录下面的index文件
    • 查找X/index.js文件
    • 查找X/index.json文件
    • 查找X/index.node文件
  1. 如果没有找到,那么报错:not found 情况三::直接是一个X(没有路径),并且X不是一个核心模块
    1. 那么你可以直接在JS文件中,打印module.paths 每个文件,每个电脑路径并不相同,但原理是相同的;他会在当前文件中,追层在node_modules查找文件夹名为X下的index.js文件.如果module.paths的路径中都没有找到,那么报错:not found
 paths:
   [ 'C:\\Users\\XXX-PC\\Desktop\\新建文件夹\\node_modules',
     'C:\\Users\\XXX-PC\\Desktop\\node_modules',
     'C:\\Users\\XXX-PC\\node_modules',
     'C:\\Users\\node_modules',
     'C:\\node_modules' ] }

模块的加载过程

结论一:模块在被第一次引入时,模块中的js代码会被运行一次
结论二:模块被多次引入时,会缓存,最终只加载(运行)一次

  • 因为每个模块对象module都有一个属性记录:loaded。 结论三:如果有循环引入,那么加载顺序是什么?

image.png Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

CommonJS规范缺点

CommonJS加载模块是同步的

  • 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
  • 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;

如果将它应用于浏览器呢?

  • 浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行
  • 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作; 所以在浏览器中,我们通常不使用CommonJS规范 webpack中使用CommonJS是另一回事,因为它会将我们的代码转成浏览器可以直接执行的代码

在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD

AMD规范

AMD(Asynchronous Module Definition(异步模块定义)的缩写)主要是应用于浏览器的一种模块化规范

  • 它采用的事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了是异步加载模块
  • AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了
  • AMD实现的比较常用的库是require.js和curl.js

require.js的使用

下载地址

定义HTML的script标签引入require.js和定义入口文件:

<script src="./lib/require.js" data-main="./index.js"></script>

data-main属性的作用是在加载完src的文件后会加载执行该文件

image.png

CMD规范

CMD(Common Module Definition(通用模块定义))规范也是应用于浏览器的一种模块化规范

  • 采用了异步加载模块,但是它将CommonJS的优点吸收了过来
  • CMD优秀的实现方案 SeaJS

SeaJS的使用

下载地址

image.png

ES Module

ES Module和CommonJS的模块化有一些不同之处:

  1. 一方面它使用了import和export关键字;
  2. 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式
  3. 采用ES Module将自动采用严格模式:use strict ES Module 使用注意
<script src="./modules/foo.js" type="module"></script>

如果直接在浏览器中运行代码,会报CORS错误,因为 Javascript 模块安全性需要. 需要通过一个服务器来测试

export关键字

导出方式

  • 方式一:在语句声明的前面直接加上export关键字
export const obj = {}
  • 方式二:将所有需要导出的标识符,放到export后面的 {}中 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的; 所以: export {name: name},是错误的写法
const obj = {}
function foo () {
}
export {obj, foo}
  • 方式三:导出时给标识符起一个别名
const obj = {}
function foo () {
}
export {obj, foo as test}

import关键字

  • 方式一:import {标识符列表} from '模块'; 注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
  • 方式二:导入时给标识符起别名
  • 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上

export和import结合使用

export { foo as bar } from './bar.js'

为什么要这样做呢?

  • 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中
  • 这样方便指定统一的接口规范,也方便阅读

default用法

有一种导出叫做默认导出(default export),在一个模块中,只能有一个默认导出

export default const obj = {}
export { obj as default }
  • 默认导出export时可以不需要指定名字;
  • 在导入时不需要使用 {},并且可以自己来指定名字;
  • 它也方便我们和现有的CommonJS等规范相互操作

import函数

通过import加载一个模块,是不可以在其放到逻辑代码中的;

  • 因为ES Module在被JS引擎解析时,就必须知道它的依赖关系
  • 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况
  • 甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值
// 错误写法
if (true) {
    import foo from './foo.js'
}

正确的动态的来加载某一个模块import() 函数

if (true) {
    import(./foo.js).then(res => {
       res.foo()
    })
}

import.meta(在ES11)是一个给JavaScript模块暴露特定上下文的元数据属性的对象;它包含了这个模块的信息,比如说这个模块的URL

ES Module解析流程

解析流程文章

ES Module的解析过程可以划分为三个阶段

  1. 构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)
    • 这也就是为什么本地ftp://会报错了,因为它本质是需要根据地址进行请求下载文件的,需要使用http/https协议
  2. 实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
    • 在这一阶段,内部的代码并没有开始运行,只会解析导入和导出的部分,并给相应的导入导出变量分配对应的内存空间,但是并没有进行赋值,这些变量都只是undefined
  3. 运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中

image.png

image.png

CommonJS 与 ES Module 是否互相兼容

  1. 在浏览器环境中不能互相兼容
  2. 在node环境中区分版本,在支持ESM的版本中可以互相兼容 3.在基于webpack的开发环境中,因为webpack对它们进行了支持,所以是互相兼容的

npm包管理工具

npm init -y 创建package.json

  • 全局安装 npm install webpack -g;
  • 项目(局部)安装(local install): npm install webpack
  • npm cache clear 清空缓存
  • npm rebuild 根据package重新构建 npm命令

package.json 常见属性

  • name 项目名称
  • version 项目版本号
  • description 描述信息
  • author 作者相关信息
  • license 开源协议
  • private 是否私有
  • main 设置程序的入口; 这个入口和webpack打包的入口并不冲突;比如我们使用axios模块 const axios = require('axios');实际上是找到对应的main属性查找文件的;
  • scripts 配置脚本命令,以键值对的形式存在; 配置后我们可以通过npm run 命令的key来执行命令;npm start和npm run start是等价的,对于常用的 start、 test、stop、restart可以省略掉run直接通过 npm start等方式运行;
  • dependencies 无论开发环境还是生成环境都需要依赖的包 npm install xxx --save 简写npm install xxx -S
  • devDependencies 生成环境是不需要的 npm install xxx --save-dev 简写npm install xxx -D
  • peerDependencies 是对等依赖,也就是你依赖的一个包,它必须是以另外一个宿主包为前提的;比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom
  • engines 用于指定Node和NPM的版本号,所在的操作系统 "os" : [ "darwin", "linux" ],在安装的过程中,会先检查对应的引擎版本,如果不符合就会报错;
  • browserslist 用于配置打包后的JavaScript浏览器的兼容情况,否则我们需要手动的添加polyfills来让支持某些语法,也就是说它是为webpack等打包工具服务的一个属性

^x.y.z:表示x是保持不变的,y和z永远安装最新的版本; ~x.y.z:表示x和y保持不变的,z永远安装最新的版本;

npm install 原理

image.png 如何判断package-lock.json和package.json是否通过了一致性检查?

在npm@5.4.2版本后的表现为

  1. 如果改了package.json,且package.json和lock文件不同,那么执行npm i时npm会根据package中的版本号以及语义含义去下载最新的包,并更新至lock。
  2. 如果两者是同一状态(1.package和lock中包的版本一致;2.lock中包的版本大于package中包的版本),那么执行npm i 都会根据lock下载,不会理会package实际包的版本是否有新的。

yarn工具

image.png

cnpm工具

查看npm镜像
npm config get registry # npm config get registry 安装cnpm npm install -g cnpm --registry=registry.npm.taobao.org

npx工具

npx是npm5.2之后自带的一个命令,npx的作用非常多,但是比较常见的是使用它来调用项目中的某个模块的指令。它会到当前目录的node_modules/.bin目录下查找对应的命令,再查找全局环境

局部命令的执行

如何使用项目(局部)的webpack,常见的是两种方式

  • 明确查找到node_module下面的webpack ./node_modules/.bin/webpack --version
  • 在 scripts定义脚本,来执行webpack;"scripts": { "webpack": "webpack --version" }
  • npx webpack --version

npm发布自己的包

  1. 注册npm账号 npm (npmjs.com)
  2. 登录 npm login
  3. 修改package.json
"keywords":["react, vue"]
"homepage":"url"
"repository":{
    "type":"git",
    "url":"url"
}
  1. 发布到npm registry npm publish 更新仓库:
  2. 修改版本号
  3. 重新发布

删除发布的包: npm unpublish 让发布的包过期:npm deprecate

JSON

JSON.stringify()

  • 方法将一个 JavaScript 对象或值转换为 JSON 字符串;
  • 如果指定了一个 replacer 函数,则可以选择性地替换值;
  • 如果指定的 replacer 是数组,则可选择性地仅包含数组指定的属性;
  • 它还可以跟上第三个参数space

image.png 如果执行JSON.stringify的对象本身包含toJSON方法,那么会直接使用toJSON方法的结果

image.png

JSON.parse()

方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象;提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)。

image.png

cookie

cokkie是一种小型文本文件,浏览器会在特定的情况下携带上cookie来发送请求,我们可以通过cookie来获取一些信息;

cokkie总是保存在客户端中,按照客户端的存储位置,cookie分为内存cookie和硬盘cookie

  • 内存cookie由浏览器维护,保存在内存中,浏览器关闭时cookie就会消失;没有设置过期时间,默认情况下cookie是内存cookie,在关闭浏览器时会自动删除;
  • 硬盘Cookie保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理;

cookie 属性

cookie生命周期

默认情况下cookie是内存cookie,也就是在浏览器关闭时会自动被删除;

我们可以通过设置下面属性来决定过期时间

  • expires 设置的是Date.toUTCString()
  • max-age 设置过期的秒钟

cookie的作用域

Domain:指定那些主机可以接受cookie

  • 如果不指定 默认是origin 不包括子域名
  • 指定Domain,则包含子域名. 如Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org) Path:指定主机下那些路径可以接受cookie Path=/docs, 则/docs以及子路径/docs/xx/xx/... 都会匹配

JS 设置cookie

获取cookie document.cookie

设置cookie,前提是服务器设置了httpOnly=false document.cookie = "name=luo"

设置cookie,同时设置过期时间(默认秒钟) document.cookie = "name=luo;max-age=10"

认识BOM

我们可以将BOM看成是连接JavaScript脚本与浏览器窗口的桥梁。 BOM主要包括以下对象模型:

  • window:包括全局属性、方法,控制浏览器窗口相关的属性、方法;
  • location:浏览器连接到的对象的位置(URL)
  • history:操作浏览器的历史;
  • document:当前窗口操作文档的对象; window对象在浏览器中有俩个身份:
  1. 全局对象;我们知道ECMAScript其实是有一个全局对象的,这个全局对象在Node中是global;在浏览器中就是window对象
  2. 浏览器窗口对象;作为浏览器窗口时,提供了对浏览器操作的相关的API;

EventTarget

在IE7/8中需要使用attachEvent/detachEvent Window继承自EventTarget,所以会继承其中的属性和方法;

  • addEventListener:注册某个事件类型以及事件处理函数;
  • removeEventListener:移除某个事件类型以及事件处理函数;
  • dispatchEvent : 派发某个事件类型到EventTarget上; 默认的事件监听

Location对象常见的属性

  • href:当前window对象的整个URL juejin.cn/editor/draf…
  • protocol:当前协议 https
  • host:主机地址 juejin.cn
  • hostname:主机地址(不带端口) juejin.cn
  • port:端口 ""
  • pathname:路径 /editor/drafts/xxxx
  • search 查询字符串 ?.....
  • hash:哈希值 '#/home/....?...'
  • username:URL中的username
  • password:URL中的password
  • assign():赋值一个新的URL,并且跳转到该URL中;
  • replace():打开一个新的URL,并且跳转到该URL中(不同的是不会在浏览记录中留下之前的记录);
  • reload:重新加载页面,可以传入一个Boolean类型; image.png

history对象常见属性和方法

history对象允许我们访问浏览器曾经的会话历史记录。

  • length 会话中的记录条数
  • state 当前保留的状态值
  • back() 返回上一页
  • forward() 前进下一页
  • go() 加载历史中的某一页
  • pushState(state, title, url):打开一个指定的地址
  • relaceState(state, title, url):打开一个新的地址,并且使用replace

认识DOM和架构

DOM给我们提供了一系列的模型和对象,让我们可以方便的来操作Web页面。 image.png

Node节点

所有的DOM节点类型都继承自Node接口

Document

image.png

Element

    const box1 = document.querySelector(".box1")
    console.log("box", box1.children) // 只包含element元素 [span, div, span]
    console.log("box", box1.childNodes) // 变量为 NodeList 类型,且为只读  [text, span, text, div, text, span]

    console.log("box", box1.className) // 返回字符串 box1 box
    console.log("box", box1.classList) // 返回类数组[box1, box]

    console.log("box", box1.clientHeight) //box1 大小
    console.log("box", box1.clientWidth) //box1 大小

    console.log("box", box1.clientLeft) //边框的大小
    console.log("box", box1.clientTop) //边框的大小
    console.log("box", box1.clientLeft) //边框的大小
    console.log("box", box1.clientTop) //边框的大小

    const attr = box1.getAttribute("id") //获取属性
    box1.setAttribute("id", "box2") //设置属性

事件监听

在Web当中,事件在浏览器窗口中被触发,并且通过绑定到某些元素上或者浏览器窗口本身,那么我们就可以 给这些元素或者window窗口来绑定事件的处理程序,来对事件进行监听。

事件监听的方式

  • 在script中获取元素直接监听 // DOM 0级
  • 通过元素的on来监听事件 // DOM 0级
  • 通过EventTarget中的addEventListener来监听 // DOM 2级

事件冒泡和事件捕获

我们可以想到一个问题:当我们在浏览器上对着一个元素点击时,你点击的不仅仅是这个元素本身;

我们会发现默认情况下事件是从最内层的span向外依次传递的顺序,这个顺序我们称之为事件冒泡 (Event Bubble)。

事实上,还有另外一种监听事件流的方式就是从外层到内层(body -> span),这种称之为事件捕获(Event Capture);

addEventListener的第三个元素 就是决定是冒泡还是捕获,默认是false为冒泡;如果设置了两个监听事件捕获优先于事件冒泡执行 image.png

事件对象event

当一个事件发生时,就会有和这个事件相关的很多信息;这些信息会被封装到一个Event对象中;

常见的属性:

  • type:事件的类型
  • target:当前事件发生的元素
  • currentTarget:当前处理事件的元素(在事件冒泡时触发就会跟target不相同, 比如父元素也会触发子元素冒泡上来的事件,此时在父元素中currentTarget就是自身,target就是子元素)
  • offsetX,offsetY:点击元素的位置
  • clientX,clientY:事件发生在客户端(可视区,不包含滚动区域的大小)的位置
  • pageX,pageY:事件发生在客户端的位置(包含被滚动的区域)
  • screenX,screenY:事件发生相对于计算机屏幕的位置 常见的方法:
  • preventDefault: 取消事件的默认行为
  • stopPropagation:阻止事件的进一步传递 事件参考 | MDN (mozilla.org)

鼠标事件

  • mouseenter和mouseleave: 不支持冒泡;进入子元素依然属于在该元素内,没有任何反应
  • mouseover和mouseout: 支持冒泡;进去元素的子元素时,先调用父元素的mouseout再调用子元素的mouseover,因为支持冒泡所以会将mouseover传递到父元素中

键盘事件

  • down 事件先发生
  • poress 发生在文本被输入时
  • up 发生在文本输入完成

window事件

window.addEventlistener("DOMContentLoaded", ()=>{
    // 浏览器已完全加载HTML,并构建了DOM树,但img和样式表之类的外部资源可能尚未加载完成
})
window.load = ()=>{
    // 完成了HTML,和所有外部资源的加载
}