JavaScript 基础

Class 类

  1. 类默认使用严格模式。
  2. 不存在变量提升,即不能在类声明前使用类。

基本内容

  1. new 关键字

    创建类的实例,做了以下事情:

    1、创建一个空对象(即 {}

    2、为空对象添加属性 __proto__,将该属性指向构造函数的原型对象

    3、将创建的对象作为 this 的上下文,执行构造函数(为这个对象添加属性)。

    4、判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

  2. class 关键字

    定义类,例:

    class one{
        a = "hello";
        b = "world";
        f = function(){console.log(this.a)};
        g = function(){console.log(this.b)}
    }
    var ex = new one  //创建实例
    ex.f()  //输出:hello
    ex.g()  //输出:world
    
  3. constructor 构造方法

    类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显示定义,JS 将默认创建一个空的 constructor 方法。

    constructor 方法默认返回实例对象(即 this ),也可以指定返回另外一个对象,例如:

    class one{
        constructor(){
            return Object.create(null)
            // 默认return Object.create(this)
      }
    }
    new one() instanceof one  // 输出:false
    // new one instanceof one  // 输出:true
    
  4. extends 继承

    继承父类,如:

    class parent {
        a = "hello";
        f = function(){console.log(this.a)}
    }
    class child extends parent {
        a = "world";
        g = function(){console.log(this.a)}
    }
    var ex = new child
    ex.f()  //输出:world
    ex.g()  //输出:world
    
  5. super关键字

    即可以作为函数使用也可以作为对象使用。

    当作为函数调用时,代表父类的构造函数。

    当作为对象时,在普通方法中指向父类的原型对象;在静态方法中指向父类。

  6. new.target 属性

    判断函数是否作为构造函数被调用。如:

    function Obj(){
        if(new.target){
            console.log('将我作为构造方法调用了')
        }else{
            console.log('将我作为普通方法调用了')
        }
    }
    

示例

  1. 创建类

    class One {  //类不存在变量提升
        args = null  //实例属性
        static a = 0  //类的静态属性,不会被类的实例继承
        constructor(args){//构造方法,每个类必须有
            this.args = args  //实例化时可以为属性args重新赋值
        }
        f(arg){ console.log(arg, this.args) }
        get g(){ console.log('get取值关键字')}
        set g(arg){ console.log('set存值关键字', arg) }
        static h(){console.log('静态方法')}  //类的静态方法,不会被类的实例继承,但能被子类继承
    }
    const ex1 = new One([1,2])  //创建One类的实例ex1
    ex1.args  //实例属性,重新赋值为[1, 2]
    ex1.f(1)  //使用实例方法
    One.prototype  //查询类的原型对象
    ex1.__proto__  //查询实例的原型对象
    One.prototype.constructor === One  //原型的constructor属性指向类本身
    One.prototype  ===  ex1.__proto__  //类的原型对象与实例的原型对象相等
    Object.keys(One.prototype)  //查询对象中所有可枚举属性的名字,类内部定义的方法都是不可枚举的,而ES5使用One.prototype.f = function(){}定义的方法是可枚举的。
    Object.getOwnPropertyNames(One.prototype)  //查询原型中所有的非继承的属性的名字
    ex1.hasOwnProperty('args')  //this定义的属性都是实例自身的属性,所以返回true
    ex1.hasOwnProperty('f')  //实例的方法定义在原型上,所以返回false,类的所有实例共享一个原型
    ex1.g  //调用get关键字定义的g(),输出:get取值关键字
    ex1.g = 33  //调用set关键字定义的g(),输出:set存值关键字
    
  2. 继承类

    class Two extends One {
        args = null  //父子同名属性父属性会被覆盖
        constructor(x, args){
            super(x)  //调用父类One的构造函数,每个继承的类必须有,super()做函数使用时指向父类的构造函数
            this.args = args
        }
        f(arg){  //父子同名方法父方法会被覆盖
            super.f(arg)  //调用父类中的f方法,super作为对象时,在普通方法中指向父类的原型对象,在静态方法中,指向父类。
            console.log(arg, this.args)
        }
    }
    const ex2 = new Two("One", "Two")  //创建One类的实例ex2
    Object.getPrototypeOf("Two")  //返回类Two的原型对象,可以判断Two是否继承自One
    Two.__proto__ === One  //子类的__proto__指向父类
    Two.prototype  //子类的原型对象
    Two.prototype.__proto__ === One.prototype  //子类的原型对象的原型对象指向父类的原型对象
    

构造函数

在 JS 中使用 new 关键字调用的函数就称为构造函数,任何函数都可以成为一个构造函数。

对象(实例):使用构造函数创建的对象(实例),例如:var a = new f()a 就是构造函数 f() 的一个对象(实例)。

构造函数与普通函数的区别

  1. 构造函数约定首字母大写。

  2. 调用方式不一样,构造函数使用 new 关键字调用,普通函数直接调用。

  3. 作用不一样,构造函数用于创建实例,以达到复用代码的作用。

  4. 构造函数使用 this 创建属性和方法,这样可以达到代码复用的目的,因为 this 会指向实例本身。

  5. 构造函数的执行流程:

    • 立即在堆内存中创建一个新的对象,这个对象就是构造函数的实例(构造函数的实例之间互不影响)。
    • 将新建的对象设置为函数中的 this
    • 逐个执行构造函数中的代码。
    • 将新建的对象作为返回值。
  6. 普通函数没有返回值,例:

    function f(){console.log("hello")}
    var fx=f()
    console.log(fx) //返回: "undefined"
    
  7. 构造函数会创建一个新对象,并将该对象作为返回值返回,例:

    function F(){console.log("hello")}
    var fx=new F()
    console.log(fx) //返回: F {}
    fx instanceof F //instanceof可以检查一个对象是否是一个类的实例。注意,所有的对象都是Object对象的实例。
    

对象

  1. 接受变量作为属性,如:

    const a = "hello"
    const obj = {a}  //等同于const obj = {a: "hello"}
    
  2. 接受函数作为方法,如:

    const obj = {
        a(){
            alert("hello")
        }
    }
    //等同于
    const obj = {
        a: function(){
            alert("hello")
        }
    }
    
  3. 表达式作为属性名:变量作为属性名时,需要用 [ ] 括起来,如:

    const a = "hello"
    const obj = { [a]: "a", ["wor"+"ld"]: "b" }
    obj //输出:{hello: "a", world: "b"}
    

属性的遍历

  1. for···in :遍历对象自身和继承的可枚举属性。

  2. Object.keys(obj):返回一个数组,包含对象自身(不包含继承的)的所有可枚举(不包含Symbol属性)属性的键名。

  3. Object.getOwnPropertyNames(obj) :返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,不包含继承的,但是包括不可枚举属性)的键名。

  4. Object.getOwnPropertySymbols(obj) :返回一个数组,包含对象自身的所有 Symbol 属性的键名。

  5. Reflect.ownKeys(obj) :返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

    注意:以上属性遍历的次序准守一下规则:

    1. 首先遍历所有数值键,按照数值升序排列。
    2. 其次遍历所有字符串键,按照加入时间升序排列。
    3. 最后遍历所有 Symbol 键,按照加入时间升序排列。

super关键字:指向当前对象的原型对象。例如:

const one = {a: "hello"}
const two = {
    a: "world",
    f(){//简写方法
        console.log(super.a)
    }
}
Object.setPrototypeOf(two, one)  //将one绑定为two的原型
two.f()  //输出:hello

注意:super 关键字只能用在对象的方法中,且方法必须为简写方法。


函数

函数参数默认值:可以为函数的参数设置默认值。例如:

function f(x, y=5){
    console.log(x + y)
}
f(3)  //输出:8

length:函数属性,返回函数没有指定默认参数的参数个数。例如:

function f(x, y=5){
    console.log(x+y)
}
console.log(f.length)  //输出:1

参数作用域:当设置了参数的默认值时,在函数初始化时参数会形成一个单独的作用域,等到初始化结束,这个作用域会消失。例如:

let x = 1
function f(x, y = x){
    console.log(y)
}
f(2)  //输出:2

rest:参数(形式为 ...变量名 ),用于获取函数的多余参数。这样就不需要使用arguments对象。例如:

function add(...values){
    let sum = 0;
    for (let value of values){
        sum += value
    }
	return sum
}
console.log(add(1, 2, 3))  //输出:6

name:函数属性,返回函数的函数名。

箭头函数

  1. 语法:var f = (参数) => {语句}

  2. 特点

    • 箭头函数没有自己的 this,它的 this 是最近一层非箭头函数的函数的 this ,若没有外层函数则指向全局对象 window 对象。例如:

      const a = {
          one: function(){
              (() => {console.log(this)})()
          }
      }
      a.one()  //输出:a对象,此时箭头函数的this是外层函数的this。
      const b = {
          one: {
              two: () => {console.log(this)}
          }
      }
      b.one.two()  //输出:window,此时箭头函数外层没有非箭头函数,所有this指向window。
      
    • 箭头函数不能做为构造函数。

    • 箭头函数不能使用 arguments 对象,该对象在函数体内不存在,可以使用 rest 参数代替。

    • 箭头函数不能使用 yield 命令,因此箭头函数不能用作 Generator 函数。

立即执行函数

  1. 概念:立即执行函数可以让函数在创建后立即执行,这种模式的本质是函数表达式在创建后立即执行。

  2. 写法:

    //写法一
    (function(){...})()
    
    //写法二
    (function(){...}())
    
    //错误写法
    function(){...}()
    //报错Uncaught SyntaxError: Unexpected token (
    //报错的原因是JS引擎看到function关键字后,认为后面跟的是函数定义语句,但在一条语句后面加上()会被当做分组操作符,而分组操作符里必须要有表达式,所以报错。
    
  3. 作用:立即执行函数会创建一个独立的作用域,作用域里面的变量外面无法访问,避免了环境污染。


letconst

代码块:在 {} 中的代码。

let:声明一个变量,该变量作用域是它所处的代码块。特点:

  • 不存在变量提升:变量提升指变量会在声明时提到代码块的顶端,这时候可以在声明变量之前使用变量而不会报错,不存在变量提升则代表不能在变量声明前使用变量,例如:

    console.log(a)  //输出undefined,变量提升
    console.log(b)  //报错变量b未定义,不存在变量提升
    var a = "hello" //变量提升
    let b = "world" //不存在变量提升
    
  • 暂时性死区:只要块级作用域存在 let 命令,该代码块就会绑定一个作用域,不受外部作用域的影响,例如:

    var a = "hello"
    {
        console.log(a)  //输出 hello
        let b = "world"
    }
    console.log(b)  //报错,变量 b 未定义,由于暂时性死区。
    
  • 不允许重复声明:不允许在同一个作用域中重复声明同一个变量。

const:声明一个只读的常量,一旦声明不允许修改。注意,const 一旦声明变量就必须立即初始化,不能留在以后赋值。例如:

const a = "hello"
const b //报错,变量b未赋值。

特点:const 声明变量对于简单数据类型来说就是指向数据本身,对于复合型数据来说是指向数据的引用地址,引用地址不能改变,但数据可以改变。


this

this 指的是函数运行时所在的环境。

绑定:

  1. 默认绑定:没有其他绑定规则存在时的默认规则。通常是独立函数调用。例如:

    function f(){alert(this)}
    f()  //  [object Window]
    //调用f()时应用了默认绑定,this指向全局对象(非严格模式下)。
    //在严格模式下this指向undefined,undefined上没有this对象,会抛出错误。
    
  2. 隐式绑定:函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。

    function f(){alert(this)}
    var obj = {f: f}
    obj.f()  //  [object Object]
    //f()是在obj对象下调用的,所以this指向obj。
    

    隐式绑定的陷阱一

    function f(){alert(this)}
    var obj = {f: f}
    var ff = obj.f
    ff()  //  [object Window]
    //注意obj.f是引用属性,赋值给ff实际上就是将f函数的指针赋值给ff,此时ff指向f函数本身,那么实际的调用关系是通过ff找到f函数进行调用,整个调用没有通过其他对象,此时f函数指向全局对象。
    

    隐式绑定陷阱二,函数传参,参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。

    var a = 10; // a 是全局对象的属性 
    function f(){console.log(this.a)} 
    function g(callback) {// callback其实引用的是f, 
        callback() // <-- 调用位置!
    } 
    var obj ={
        a: 2,
        f: f
    }
    g( obj.f ) //输出10
    
  3. 显示绑定:通过 call , apply , bind 的方式,显式的指定 this 所指向的对象。

    • function.call(object, args···):将函数 function 作为对象 object 的方法调用,args 作为参数传递给函数。

    • function.bind(object, args):返回一个新函数,该函数会作为 object 的方法调用,args 是旧函数 function 的参数。例:

    • function.apply(object, args):将函数 function 作为对象 objec t的方法调用,args 作为参数传递给函数,注意,apply 的参数 args 只能是数组。例:

  4. new 绑定:this 指向的就是对象本身。

    function F(name){this.name = name}
    var ff = new F("hello")
    ff.name  // hello
    var fff = new F("world")
    fff.name  //  world
    

    使用new调用函数时,执行了如下过程:

    • 创建(或者说构造)一个全新的对象。
    • 这个新对象会被执行 [[ 原型 ]] 连接。
    • 这个新对象会绑定到函数调用的 this
    • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

绑定优先级:如果同时应用了多种规则,四种绑定的优先级为:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

例外

  1. null 或者是 undefined 作为 this 的绑定对象传入 callapply 或者是 bind ,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  2. 箭头函数没有自己的 this ,它的 this 继承于外层代码中的 this

对象属性的描述符

概念JavaScript 对象的属性在创建的时候都带有一些特征值,这些特征值使用方括号 [[]] 括起来,且仅供内部使用,在外部不能直接访问,这些特征值被分为两类,分别是数据属性和选择器属性,统称为对象属性的描述符。

注意,一个对象的属性的描述符只能有数据属性和访问器属性中的一个。

数据属性:属性的数据属性一共有四个,分别是:

  • [[Value]]:表示这个属性的值,如上例 obj.name 的数据属性 [[Value]]hello
  • [[Writable]]:表示这个属性的可写性(即这个属性的值是否可被改变),默认情况下为 true
  • [[Enumerable]]:表示这个属性的可枚举性,默认情况下为 true
  • [[Configurable]]:表示这个属性的可配置性(即对象的属性属性是否可删除,和[[Enumerable]][[Configurable]]是否可以被修改),默认情况下为 true

访问器属性:属性的访问器属性也有四个,分别是:

  • [[Get]][[Get]] 是一个函数,在读取属性时自动调用,将函数的返回值作为属性的值,默认值为 undefined
  • [[Set]][[Set]] 是一个函数,在写入属性时自动调用,将写入的属性传入函数,并将函数传出来的值作为属性的值,默认值为 undefined
  • [[Enumerable]]:表示这个属性的可枚举性,默认情况下为 true
  • [[Configurable]]:表示这个属性的可配置性,默认情况下为 true

操作对象属性描述符的方法

  1. Object.getOwnPropertyDescriptor(obj, 'prop'):返回对象 objprop 属性的属性描述符。

  2. Object.getOwnPropertyDescriptors(obj):查看对象 obj 所有属性的属性描述符。

  3. Object.defineProperty(obj, prop, descriptor):定义或修改对象(obj) 的 属性(prop) 的描述符(descriptor)。

    //修改obj.name的属性描述符,且设置为不可修改
    Object.defineProperty(obj, 'name', {
        value: 'world',
        writable: false
    })
    obj.name //输出:world
    //在obj上新增属性,并设置描述符,当对象没有指定属性时会为其新增这个属性
    Object.defineProperty(obj, 'add', {
        value: 'hello',
        writable: false
    })
    obj.add //输出:hello
    
  4. Object.defineProperties(obj, props):定义或修改对象(obj)的一个或多个属性及其描述符(props) 。

    //修改对象obj原有的两个属性,新增一个属性
    Object.defineProperties(obj, {
        'name': {value: 'world', writable: false},
        'say': {value: function(){console.log(this.add)}},
        'add': {value: 'hello', writable: false}
    })
    obj.say //输出:hello
    
  5. 示例:定义多种访问器属性。

    //定义对象属性,且two的对象属性为访问器属性
    var mix = {}
    Object.defineProperties(mix, {
        'one': {value: 'hello'},
        '_two': {value: 'world', writable: true},
        'two': {
            get: function(){return this._two},
            set: function(arg){this._two = arg}
        }
    })
    

Iterator迭代器

概念:Iterator 迭代器是一个接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署了Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

工作流程

  1. 创建一个指针对象,指向当前数据结构的起始位置。

  2. 第一次调用指针对象的 next 方法,将指针指向数据结构的第一个成员,并返回第一个成员的信息,是一个包含 valuedone 属性的对象,value 是成员的值,done 是一个布尔值,表示遍历是否结束。

  3. 第二次调用指针对象的next方法,将指针指向数据结构的第二个成员,并返回第二个成员的信息。

  4. 不断调用指针对象的 next 方法,直到指向数据结构的结束位置。

  5. 例如:

    //一个遍历器函数
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++], done: false} :
            {value: undefined, done: true};
        }
      }
    }
    var it = makeIterator(['a', 'b']);
    it.next() // {value: "a", done: false}
    

for...of:当使用for...of循环遍历某种对象时,该循环会自动去找 Iterator 接口。

默认Iterator接口:默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性上,只要有这个属性的数据结构就都是可遍历的,原生具备这个接口的数据结构有:ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象。


XMLHttpRequest

概念:XMLHttpRequest 对象是 JavaScript 定义的用于与服务器通信的对象。所有的浏览器都有内建的 XMLHttpRequest 对象。

构造函数:XMLHttpRequest()

示例

let xhr = new XMLHttpRequest()  //创建XMLHttpRequest对象
xhr.timeout = 0  //设置请求超时时间
xhr.onload = function(res){...}  //请求成功时触发回调函数
xhr.onloadend = function(res){...}  //请求结束时触发函数
xhr.onerror = function(res){...}  //请求出错回调函数
xhr.ontimeout = function(res){...}  //请求超时回调函数
xhr.onreadystatechange = function(){
    //如果请求成功返回请求内容
    if(xhr.readyState == 4 && xhr.status ==200){
        return xhr.response
    }
}
xhr.open(method=请求方法, url=请求地址, async=是否异步) //初始化请求
xhr.responseType = ""  //设置期望返回的数据类型
xhr.setRequestHeader()  //设置请求头
xhr.send()  //发送求情

词法作用域

概念:词法作用域就是定义在词法阶段的作用域。词法作用域中,作用域由写代码时变量和函数的位置来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。注意词法作用域中,函数的作用域由函数的书写位置来决定,而不是函数的调用位置。如:

var a=1
function f(){
    var a = 2
    setTimeout(function(){console.log(a)},1000)
}
f()//输出2,因为函数定义在f内部,查找变量a时先在f内查找,未找到再去上层找

var a = 1
function g(){console.log(a)}
function f(){
    var a = 2
    setTimeout(g,1000)
}
f()//输出1,因为g定义在f外部,查找变量a时不会到f内部查找

闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包。例如:

function f(){
    var a = 1
    function g(){console.log(a)}
    return g
}
var gg = f()
gg()//输出1,此时f()在它所在的词法作用域以外执行,但依然访问到了它所在的词法作用域,可以称为产生了闭包

延迟函数的回调会在循环结束时执行,如:

for (var i=1; i<=5; i++) {
    setTimeout(function f(){console.log( i )}, i*1000 )
}
//输出5次6,虽然函数f是在五次迭代中分别定义的,但共享一个作用域,这个作用域中只有一个i,固每次都会打印出相同的值。

//使用创建词法作用域和闭包,使循环函数输出1-6
for (var i=1; i<=5; i++) {
    (function(){//每次循环都会创建一个新的匿名函数,将形成一个新的作用域,达到循环输出的目的。
        var j = i
        setTimeout(function f(){console.log(j)}, i*1000 )
    })()
}

动态作用域

this
  1. this 在任何情况下都不指向函数的词法作用域。例如:

    function g(){console.log(this.a)}
    function f(){
        var a = 2
        g()//g函数试图通过this引用f的词法作用域。
    }
    f()//输出undefined,因为this在任何情况下都不指向函数的词法作用域。
    
  2. this 不指向函数自身。

  3. this 是在运行时绑定的,而不是在编写时绑定的(箭头函数除外),它的指向取决于函数在哪里调用。

  4. 调用栈和调用位置

    function baz() {// 当前调用栈是:baz     // 因此,当前调用位置是全局作用域 
        console.log( "baz" )
        bar(); // <-- bar 的调用位置
    }
    function bar() {// 当前调用栈是 baz -> bar// 因此,当前调用位置在 baz 中
        console.log( "bar" )
        foo(); // <-- foo 的调用位置
    }
    function foo() {// 当前调用栈是 baz -> bar -> foo// 因此,当前调用位置在 bar 中
        console.log( "foo" )
    }
    baz(); // <-- baz 的调用位置
    
  5. this 的绑定规则

    • 默认绑定
    • 隐式绑定
    • 显示绑定
    • new 绑定

数据类型及检验方法

JS 的内置类型,可以分为基本类型和引用类型,如下:

  1. 基本类型:String、Number、Bollean、Null、Undefined、Symbol
  2. 引用类型:Array、Object、Function、Date、RegExp、Error、Arguments 等都属于引用类型。

注意,JS 中变量没有类型,它的值才有类型。

检验方法:

  1. typeof

    能判断:

    • String(返回 string
    • Number(返回 number
    • Boolean(返回 boolean
    • undefined(返回 undefined
    • function(返回 function
    • Symbol(返回 symbol

    不能判断:

    • 对象类型都返回 object,不会更细化,如: typeof [] 返回 object
    • null 返回 object
    • NaN 返回 number
  2. instanceof

    用来判断对象是否为某一数据类型的实例,不能判断基本数据类型(StringNumberBolleanNullUndefinedSymbol)。

  3. constructor

    返回创建此对象的构造函数的引用,不能判断 nullundefined 其它都可以(因为 nullundefined 没有构造函数)。

    使用方法:(1).constructor === Number ,返回 true

  4. Object.prototype.toString

    返回一个表示该对象的字符串,可以判断所有类型。

    使用方法:Object.prototype.toString.call(null),返回 [object Null]


解构赋值

数组(变量与解构数组顺序一致)

  1. 普通解构:只要两边的相对应的项的模式大致相同就能结构赋值(大致相同指左边的嵌套层数不能比右边多),左边比右边多时,左边多的赋值为 undefined ,左边比右边少时,右边多的值被省略。

    let [a, b, c] = [1, [2]]  //前面需要使用let、var、const或者用()括起来
    console.log(a)  //输出1
    console.log(b)  //输出[2]
    console.log(c)  //输出undefined
    
  2. 默认值解构:右边的值能够带有默认值,当左边值不够或严格等于 undefined 使用默认值。

    let [a, b=2, c=3, d=4] = [1, "hello", undefined]
    console.log(a)  //输出1
    console.log(b)  //输出hello
    console.log(c)  //输出3
    

对象(变量与解构对象的名字相同,无关顺序)

  1. 简写解构:需要两边的对象名字相同,无法匹配的赋值为 undefined

    let {a, b, c} = {b: "world", a: "hello", d: "no"}
    console.log(a)  //输出hello
    console.log(b)  //输出world
    console.log(c)  //输出undefined
    

    注意,当解构不成功时变量会被赋值为 undefined

  2. 默认值解构:右边的值能够带有默认值,当两边无法匹配或匹配值严格等于 undefined 使用默认值。

    let {a="one", b="world"} = {a: "hello"}
    console.log(a)  //输出hello
    console.log(b)  //输出world
    
  3. 一般解构:

    真实解构情况:let {a: aa, b: bb} = {a: "hello", b: "world"},左右两边的 ab 作为匹配标准,使 aahello 匹配、bbworld 匹配。生成{aa: "hello", bb: "world"}

    默认解构情况:let {a: a, b: b} = {a: "hello", b: "world"}

    简化为:let {a, b} = {a: "hello", b: "world"}

    对象解构的真实情况是将解构变量赋值给变量右边的值。

    let {a: aa, b: bb} = {a: "hello", b: "world"}
    console.log(aa)  //输出hello
    console.log(a)   //输出变量a未定义
    

字符串解构:字符串是一个类数组,可以遍历,所以也能用来解构

let [a, b, c] = 'hel'
console.log(a)  //h
console.log(b)  //e
console.log(c)  //l

函数的参数解构赋值

//数组一般解构
function add([x, y]){console.log(x+y)}
add([1,2])  //输出3
//数组默认值解构
function add([x=1, y=2]){console.log(x+y)}
add([2])  //输出4

//对象一般解构
function add({x, y}){console.log(x+y)}
add({x:1, y:2})  //输出3
//对象的默认值解构
function add({x=1, y=2}){console.log(x+y)}
add({x:2})  //输出4

用途

  1. 交换变量的值

    let x = 1
    let y = 2
    [x, y] = [y, x]
    
  2. 取出函数返回的多个值,函数一次只能返回一个值,所以返回多个值一般放在数组或对象中

    function ex1(){return ["hello", "world"]}
    let [a, b] = ex1()
    
  3. 给函数的参数赋值

    function ex1([x, y]){}
    ex1(["hello", "world"])
    
  4. 提取解构后的 JSON 数据


原型原型链

原型

  1. 原型:每个构造函数创建出来的时候系统会自动给这个构造函数创建并关联一个空的对象,这个空对象就是原型,访问构造函数的原型的方法:构造函数名.prototype

  2. 构造函数创建出来的对象,都会默认和构造函数的原型关联,即原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享。

  3. 访问构造函数的原型:构造函数名.prototype

  4. 访问构造函数实例的原型:实例._proto_

  5. 访问与原型关联的构造函数:原型.constuctor

  6. 如图:

    003

原型链

  1. 原型链:对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链。

  2. 如图:

    004

属性的搜索原则

  1. 先去对象自身属性中找,如果找到直接使用。
  2. 如果没找到去自己的原型中找,如果找到直接使用。
  3. 如果没找到,去原型的原型中继续找,找到直接使用。
  4. 如果没有会沿着原型不断向上查找,直到找到null为止。

事件

事件流和事件冒泡、事件捕获

  1. 事件流:指从页面中接收事件的顺序,根据不同的实现方式分为事件冒泡流和事件捕获流。
  2. 冒泡:指事件开始由具体的元素接收,然后逐级向上传播到不具体节点。
  3. 捕获:指事件开始由不具体的节点接收,然后逐级向下传播到具体节点。

相关方法

  1. addEventListener():用于改变事件流,默认为冒泡。

    el.addEventListener(event, function, [boolean])
       event:事件类型名称,不包括on
       function:事件处理函数
       boolean:事件流类型为捕获还是冒泡,默认为false冒泡。
    
  2. event.preventDefault():阻止浏览器执行当前事件关联的默认操作。如复选框的点击事件的默认操作是选中复选框,例,阻止复选框被选中:

    <p>点击:<input type="checkbox" id="ex1"></p>
    
    document.getElementById("ex1").addEventListener("click", e => {
        alert("复选框不会被选中")
        e.preventDefault()
    })
    
  3. event.stopPropagation():阻止事件在捕获、冒泡阶段进行传播。

  4. event.stopImmediatePropagation():类似于 stopPropagation( ) ,除此之外,它还阻止注册在这个元素上的一个事件其它处理程序。当一个元素上一个事件绑定了多个处理程序时,程序会逐个执行, stopImmediatePropagation() 会阻止其它事件的执行。例:

    <button id='ex1'>点击</button>
    
    const ex1 = document.getElementById("ex")
    ex1.addEventListener("click", () => alert("点击一"))
    ex1.addEventListener("click", () => alert("点击二"))
    ex1.addEventListener("click", e => {
        alert("到此为止")
        e.stopImmediatePropagation()
    })
    ex1.addEventListener("click", () => alert("不会被执行"))
    
  5. return false:相当于

    • event.preventDefault()
    • event.stopPropagation()
    • 停止回调函数执行并立即返回

    三者合一,除非清楚使用的目的要谨慎使用。

事件委托

事件委托是将元素的事件委托给它的父级或者更外层的元素去处理,它本身通过冒泡的机制去触发事件,这样只操作了一次 DOM,提高了程序性能。

注意,事件监听回调函数中的 e.target 指向的是触发这个事件的元素,而不是父元素。


async示例

function one(args){
    return new Promise(resolve => {
        resolve(args)
    })
}
function two(args){
    return new Promise(resolve => {
        resolve(args)
    })
}
async function example(args){
    const aa = await one(args)  //await后面是Promise对象对象时,返回该对象的结果,如果有一个Promise对象为rejected则代码停止执行;如果不是直接返回值
    const bb = await two("world")//
    return aa + bb  //return的返回值是then方法的回调函数的参数
}
example("hello")
    .then(res => console.log(res))  //async函数返回一个Promise对象
    .catch(error => console.log(res))  //捕获async内部错误

Generator生成器示例

示例一:

function* example(args){  //function关键字与函数名之间的*号是Generator的标志
    console.log(args)
    yield args  //yield、return关键字指定函数的内部状态,调用next()方法每次移动一个状态
    console.log('two')
    let arg = yield 'two'  //无论 yield 是否有返回值,下一个 yield 的 next 传入的参数会作为本次yield的返回值
    console.log(arg)
    yield 'thr'  //可以向next()传入参数,参数会作为上一个yield的返回值。
    return 'ending'  //next()方法走到这里终止,状态不再改变
}
const ex1 = example('one')  //传入参数,并运行Generator
ex1.next( res => {console.log(res)})  //执行一个yield之前的代码,并接受yield的返回值
ex1.next()  //向yield传参数
ex1.next('thr')  //可以向next传参,参数作为上一个yield表达式的返回值。
for(let i of ex1){console.log(i)}  //可以遍历yield执行

示例二:

function* example(){
    console.log("one")
    yield 'one'
    try{
        console.log("two")
        yield 'two'
    }catch(error){
        console.log('内部捕获错误', error)
    }
    console.log("thr")
    yield 'thr'
}
const ex1 = example()
ex1.next()  //throw抛出的错误若要被Generator内部捕获,必须执行一次next()
ex1.throw('错误')  //抛出错误,会执行一次next(),且将Generator变为执行完毕。抛出错误时,若Generator内部没有try...catch代码块,错误会被外部try...catch捕获,或默认错误程序捕获。
try{  //内部不定义try...catch时外部定义的try...catch,当二者同时存在时优先被外部捕获。
    ex1.throw('错误')
}catch(error){
    console.log('外部捕获错误', error)
}

实现斐波那契:

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

Promise 示例

const ex2 = Promise.resolve([args]) //返回一个resolved是fulfiled状态的Promise对象
const ex3 = Promise.reject([args])  //返回一个resolved是rejected状态的promise对象
const ex1 = new Promise((resolve, reject) => {
    if(true){
        resolve('成功')  //将Promise变为fulfiled的resolved状态,并返回值被then中的回调函数接收
    }
    else{
        reject('失败')  //将Promise变为rejected的resolved状态,并返回值被then中的回调函数接收
    }
}).then(  //then在当前所有的同步任务执行完后立即执行,先于计时器函数。then会在状态变为resolved时自动执行
    success => {
        console.log(a)  //打印一个不存在的变量a,使错误被catch接收
        console.log(success)  //打印resolved状态的返回值
    }, 
    error => {
        console.log(b)  //打印一个不存在的变量b,使错误被catch接收
        console.log(error)  //打印rejected状态的返回值
    }
)
.catch(error => console.log(error))  //接收代码中出现错误,也可以在then中没有接收rejected状态的回调函数的时候,接收rejected状态的返回值。
.finally(console.log("end"))  //不管状态如何都会执行,回调函数不接受任何参数

const ex4 = Promise.all([ex1, ex2, ex3])  //将多个Promise实例包装成一个,当所有的实例状态都变为fulfilled的resolved时,将所有实例的返回值组成的数组传递给then的回调函数。当有一个实例的状态变为rejected的resolved时,ex4的状态就变为rejected的resolved,将那个实例的返回值传递给ex4回调函数。

const ex5 = Promise.race([ex1, ex2, ex3])  //将多个Promise实例包装成一个,当其中一个实例的状态改变时,ex5就跟着起改变,并将这个实例的返回值传递给回调函数。

const ex6 = Promise.allSettled([ex1, ex2, ex3])  //将多个Promise实例包装成一个,当所有的实例状态都变为resolved时,将所有结果组成的数组给回调函数

异步编程

进程与线程

  • 进程:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。可以在计算机的任务管理器中查看进程,如:

    005

  • 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。可以在计算机的任务管理器中查看线程数,如:

    006

  • 进程和线程:进程是线程的容器,一个进程中可以拥有多个线程,这些线程共用进程中的资源,如数据、内存等;一个程序运行时计算机会为其分配进程(一个程序可以有多个进程),进程中的线层进行逻辑运算以实现程序的效果。

浏览器:

  • 浏览器进程:浏览器是多进程的,其主要进程有:

    1. Browser进程:浏览器的主进程,只有一个,主要用来:
      • 负责浏览器页面显示,与用户交互(就浏览器本身的功能)。
      • 负责各个页面的管理,创建和销毁其它进程。
      • 将浏览器渲染进程得到的结果绘制到用户界面上。
      • 网络资源的管理、下载等。
    2. 第三方插件进程:每种类型的插件对应一个进程,仅在插件使用时才会创建。
    3. GPU进程:最多一个,用于3D绘制等。
    4. 浏览器渲染进程:默认每个页面一个进程,这样可以保证各页面间互不影响,可以达到一个页面崩溃了不会影响其他页面等。
  • 浏览器渲染进程:也称浏览器内核,是多线程的,主要包含以下线程:

    1. GUI渲染线程

      • 负责浏览器界面渲染,如解析HTML、CSS、构建DOM数和RenderObject树、布局、绘制等。
      • 当界面需要重绘或某种操作引发回流时,该线程就会执行。
      • GUI渲染线程与JS引擎线程是互斥的,即当JS引擎执行时GUI渲染线程会被挂起,GUI更新会被保存在一个队列中等待JS引擎空闲时立即被执行。
    2. JS引擎线程

      • JS引擎线程也成JS内核,负责处理JavaScript脚本程序。
      • 一个页面(Renderer进程)中无论何时都只有一个JS线程处理JavaScript程序,这也是为什么JavaScript被称为单线程的原因。
      • 注意,由于JS引擎线程与GUI选软线程是互斥的,所以如果JS执行过长会造成页面渲染阻塞。
    3. 事件触发线程

      • 归属于浏览器而不是JS引擎,JS引擎的协助线程,当JS引擎执行代码块如setTimeout时(也可以说是浏览器渲染进程的其它线程,如鼠标点击事件、AJAX异步请求等),会将对应的任务添加到事件触发线程中,当对应的事件符合触发条件时,事件触发线程会将事件添加到处理队列的末尾,等待JS引擎处理。例如:

        function one(){console.log(1)}
        function two(){console.log(2)}
        function thr(){console.log(3)}
        function fou(){console.log(4)}
        one()                  //two会被读取放入事件触发线程中
        setTimeout(two, 30)    //当过30ms后会将two放入处理队列末尾,等待JS引擎来执行
        setTimeout(thr(), 40)  //注意thr()会立即执行
        fou()
        输出:1,3,4,2
        
    4. 定时器触发线程

      • setInterval与setTimeout所在的线程,浏览器中的的定时计数器不是由JS引擎线程计数的(JS引擎线程是单线程的,如果用来计数会导致线程堵塞),当JS引擎线程执行setInterval与setTimeout所在的代码块时,定时器触发线程开始计时,等计时结束后将任务添加到处理队列末尾。
    5. 异步HTTP请求线程

      • XMLHttpRequest连接成功后浏览器会新开的一个线程,当检测到状态变更时,如果设置有回调函数,异步HTTP请求线程会产生状态变更事件,将这个回调函数放入事件队列末尾。
  • 浏览器渲染页面的流程

    1. Browser进程收到用户请求,获取加载页面需要的内容,然后将任务通过浏览器渲染进程接口传递给浏览器渲染进程。

    2. 浏览器渲染进程接收到消息后进行渲染,最后将结果传回Browser进程。浏览器渲染进程分为以下几步:

      • 解析HTML建立DOM树

      • 解析CSS构建render树

      • 将DOM树与render树结合

        注意:CSS加载不会阻塞DOM树解析,但会阻塞最终DOM树与render树的结合。

    3. Browser进程接收到浏览器渲染进程的结果,然后将结果发送给GPU进程

    4. GPU进程将结果合成,显示在屏幕上。然后出发load事件。

    5. 然后处理页面的JS程序

JS的运行机制:

  • JS执行任务时将任务分为同步任务和异步任务,任务进入执行栈时会判断任务是同步任务还是异步任务,若是同步任务则进入主线程直接执行,若是异步任务的话会进入事件触发线程,并注册相应的回调函数,当事件触发后将任务放入事件任务列队,当主线程的任务执行完毕后执行事件任务列队中的任务。JS引擎存在一种机制,会不断检查主线程是否为空,一旦主线程为空就会检查任务队列是否有待执行的任务。

  • 流程图

    007

异步与同步

  • 同步模式:按照任务列队依次执行任务的模式。例如:

    function one(){console.log(1)}
    function two(){console.log(2)}
    function thr(){console.log(3)}
    one()
    two()
    thr()
    //函数会从上向下一次执行,输出1,2,3
    
  • 异步模式:不按照任务列队执行任务的模式。

    function f(){console.log("异步执行")}
    function one(){console.log(1)}
    function two(){
        console.log(2)
        setTimeout(f, 30)
    }
    function thr(){console.log(3)}
    one()
    two()
    thr()
    //执行流程:
    //执行one()
    //执行two(),执行two()内的console.log(2),发现setTimeout()是任务任务,放入任务列队等待执行
    //执行thr()
    //此时主线程任务全部执行完毕,查看任务列队是否有任务待执行,发现任务,执行setTimeout()
    //所以输出:1,2,3,异步执行
    
  • 单线程:JavaScript的执行环境是单线程的,按理论来说JS不可能存在异步模式,事实上也是如此,但浏览器是多线程的,所以可以利用浏览器的多线程使JS看起来像是多线程。

  • 异步实现

    1. 单线程:JavaScript的执行环境是单线程的。
    2. 理论:JS是单线程的这个前提一直没有改变,但可以依靠浏览器的多线程实现异步,JS引擎执行任务时会判断任务是同步任务还是异步任务,若是同步任务则直接执行,异步任务则放入另一个列队,等同步任务全部执行完毕,再去执行异步任务,如setTimeout()就是异步任务,在执行同步任务时会被识别,等同步任务全部执行完毕后执行,所以若想生成异步则需要想办法让任务被识别为异步任务。
  • 异步实现方法

    1. 回调函数

      • 回调函数:回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就称这个函数是回调函数。

        注意:从回调函数的定义可以看出,回调函数跟异步并没有必然关系(回调函数就是一个普通的函数)。只是执行异步操作的大多都是回调函数,这时回调函数才和异步产生了联系。例如:

        a、同步回调

        function f(){console.log("同步回调")}
        function one(){console.log(1)}
        function two(y){
            console.log(2)
            y()
        }
        function thr(){console.log(3)}
        one()
        two(f)  //此时two(f)中的f就是回调函数
        thr()
        //输出:1,2,同步回调,3
        

        b、异步回调

        function f(){console.log("异步回调")}
        function one(){console.log(1)}
        function two(y){
            console.log(2)
            setTimeout(y, 30)
        }
        function thr(){console.log(3)}
        one()
        two(f)  //此时two(f)中的f就是回调函数
        thr()
        //输出:1,2,3,异步回调
        //这时f()作为回调函数被异步调用了
        

        从上面两个例子看出,异步与回调并没有必然关系,回调函数只是将函数作为参数使用的一种方法,回调函数是否能异步与调用它的函数是否是异步有关。在同步回调例子中调用回调函数的函数是同步函数,所以回调函数同步执行;在异步回调的例子中调用回调函数的函数是异步函数,所以回调函数异步执行。

      • 回调函数的优点:

        a、代码拆分,提高代码的复用性

        b、实现异步调用,因为有些操作需要异步执行,这时可以利用回调函数在某个事件触发的时候来调用函数

      • 回调函数的缺点:

        a、丢失线性性(回调地狱),多层回调函数会导致嵌套的层数过深,使代码纵向发展,不宜阅读和理解

        b、缺乏可信任性,控制反转导致的一系列信任问题。

        • 控制反转指调用回调函数的代码是第三方的代码,不在你的直接控制之下,如$.ajax()就是第三方的代码,当无法控制的第三方代码执行回调函数的时候,会存在一些问题,如:

          1、调用回调过早,2、调用回调过晚,3、调用的次数过多或过少,4、未能把所需要的参数传递给回调函数等,5、吞掉可能出现的错误或异常。

          这就会导致信任链断裂,如果没有采取措施解决这些控制反转导致的信任问题,代码中就会隐藏Bug。

    2. 事件监听

    3. Promise

    4. Generator

    5. Async/Await

几个异步 API 优劣

  • 回调 不是异步Api,是一种异步编程的实现手段。
  • Promise 解决 回调 的回调地狱。
  • Generator 可以交出函数的执行权。
  • Async 是 Generator 语法糖,解决了 Promise 链式调用的不直观性,可读性接近于同步代码。

模块化

模块化是随着前端项目越来越大,代码越来越多,为了解决命名冲突、提高代码复用性和维护性等问题而产生的技术。

常用的有以下方案:

  1. IIFE:利用立即执行函数和闭包。

  2. CJS:即 common.js 规范(社区标准),在 node.js 中它是标准规范。

    使用 module.exportsexports (两者不能同时存在,前者优先级较高)导出,require 导入。

    exports 相当于 module.exports={},不能直接赋值(exports={a: 1} 这样不行),需要这样 exports.a = 1

    module.exports 不仅可以导出对象,还能导出函数等。

    特点:

    1. 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果(运行时加载),由于导出的数据是拷贝的,导致模块内的修改不会影响拷贝值,代码出错不容易排查。
    2. 以同步方式加载模块,并且模块加载的顺序,按照其在代码中出现的顺序,不会将模块中require提升到顶部。
    3. require 可以放在块级作用域或条件语句中。

    由于 CJS 规范的模块加载是同步(只有加载完成后,才执行后面的操作),这在 node.js 中没有问题,但在浏览器端,由于加载模块都是依赖于网络的,所以就有了新的规范。

  3. AMD:异步加载模块的一个比较流行的方案。主要用于浏览器端,其中最具代表性的就是 require.js,它的核心原理是通过动态 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。

    AMD 方案可以异步加载,依赖前置(依赖的模块全部加载完毕才会运行当前模块)。

    使用 define 定义模块并使用 exports 导出,使用require 导入模块(详细语法 可以看require.js 的文档。)

  4. CMD:异步加载模块的一个比较流行的方案。主要用于浏览器端,其中最具代表性的就是 sea.js

    CMD方案可以异步加载,按需加载(即当需要用到某模块依赖的时候才会去加载)。

    使用 define 方法利用 require 参数导入模块,使用 exportsmodule.exports 导出模块,使用 seajs.use 使用模块,(详细语法 可以看sea.js 的文档。

  5. ESM:是 JavaScript 官方的标准化模块系统,在 ES6 开始支持,当下几乎所有浏览器都已经提供了支持。

    使用 exportexport default 导出数据,import 导入数据。

    export default 导出的数据能直接使用,且一个模块中只能有一个。

    export 导出的数据在一个对象上,一个模块中不限制数量。

    特点:

    1. 不像 CJS,ESM模块导入的值不是拷贝是引用。
    2. ESM标准模块加载器是异步的,读取到脚本后不会立即执行,而是先进入编译阶段进行模块解析,检查模块上调用了 importexport 的地方,并以此类推的把依赖模块一个个异步、并行地进行下载。在此阶段 ESM 加载器不会执行任何依赖模块代码,只检查语法错误、确定模块依赖关系、确定模块输出和输入的变量。最后 ESM 会进入执行阶段,按照顺序执行各模块脚本。所以 import 会自动提升到代码的顶层。
    3. ESM 默认使用严格模式(use strict),因此在 ESM 模块中的 this 不指向全局 window对象,而是undefined,且变量在声明前无法使用。
    4. import 不可以放在块级作用域或条件语句中。如果要动态导入,需要使用到 import()
  6. UMD:通用模块定义,它是对 CJS、AMD、CMD 的兼容,使得各个版本都能正常运行。

    核心代码:

    ((root, factory) => {
      if (typeof define === 'function' && define.amd) {
        // AMD
        define(factory);
      } else if (typeof define === 'function' && define.cmd){
        // CMD
        define(function(require, exports, module) {
          module.exports = factory()
        })
      } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = factory();
      } else {
        // 都不是直接挂载在全局对象上
        root.umdModule = factory();
      }
    })(this, () => {
      // return 需要定义的模块
      return {
        name: 'randy'
      }
    });
    

    可以看到,defineAMD/CMD 语法,而 exports 只在 CommonJS 中存在,它在定义模块的时候会检测当前使用环境和模块的定义方式,如果匹配就使用其规范语法,全部不匹配则挂载再全局对象上,可以看到传入的是一个 this ,它在浏览器中指的就是 window ,在服务端环境中指的就是 global ,使用这样的方式就能兼容各种模块了。


算法相关名词

算法:算法是为了解决问题如何设计程序的思想,不是程序本身,如 数组求和,可以使用循环的方式去编写程序解决问题,这个循环就是算法。

时间复杂度:描述一个算法在运行时的所花费的时间度量,用 O 表示,具体来说就是程序中所有代码行数执行的时间,然后求极限,如:

const a = 1  // 运行时间 T, T 是一行代码的运行时间,由于它在不同环境下可能不同,所以使用 T 表示
const b = 2  // 运行时间 T
// 合计运行时间 2T,常量求极限为 2T,时间复杂度记为 O(T) 
let a = 0 // T
for(let i; i < n; i++){ // Tn + T
    a += i // Tn
}
// 合计运行时间 2T + 2Tn, 根据 n 求极限为 2Tn,时间复杂度记为 O(n)

空间复杂度:描述一个算法在运行过程中临时占用的存储空间度量,用 O 表示,具体来说就是程序中所有代码执行时所申请的存储空间,然后求极限,如:

const a = 1  // 临时空间 S
const b = 2  // 临时空间 S
// 合计临时空间是 2S,常量求极限为 2S,空间复杂度记为 O(S)
const arr = new Array(n) // 临时空间是 Sn
for(let i; i < n ; i++){
    arr[i] = i
}
// 合计临时空间是 Sn,根据求极限为 Sn,空间复杂度记为 O(n)