javascript笔记(二)

39 阅读50分钟

this

this 是一个指针型变量,它动态指向当前函数的运行环境。

  • 在全局作用域下,this 始终指向全局对象 window,无论是否是严格模式

    console.log(this)
    
  • 函数内的 this

    1. 严格模式下:

      //严格模式下
      function test() {
          "use strict"
          console.log(this)
      }
      test() //undefined
      window.test() //window
      

      直接 test()调用函数,this 指向 undefined,window.test()调用函数 this 指向 window。

    2. 非严格模式下:

      function test() {
          console.log(this)
      }
      test() //window
      window.test() //window
      

      非严格模式下,通过 test()和 window.test()调用函数对象,this 都指向 window。

  • 被嵌套的函数独立调用时,this 默认指向 window

    var obj = {
        a: 2,
        foo: function () {
            function test() {
                console.log(this) // window
            }
            test()
        }
    }
    obj.foo()
    
  • 隐式绑定 对象内部方法的 this 指向调用这些方法的对象,也就是谁调用就指向谁

    let obj = {
        name: "小明",
        skill: function () {
            console.log(this.name)
        },
        obj2: {
            name: "小红",
            skill2: function () {
                console.log(this.name)
            }
        }
    }
    
    obj.skill() //小明
    obj.obj2.skill2() //小红
    
  • 隐式丢失

    1. 被隐式绑定的函数丢失了绑定对象,从而默认绑定到 window

      function foo1() {
          console.log(this) // window
      }
      var obj6 = {
          a: 2,
          foo: foo1
      }
      var bar = obj6.foo // 在这并未执行方法
      bar() // 在这执行了方法
      
    2. 参数传递

      function foo3() {
          console.log(this)
      }
      function bar1(fn) {
          // 默认赋值 fn = obj7.foo
          fn() // window
      }
      var obj7 = {
          a: 1,
          foo: foo3
      }
      bar1(obj7.foo)
      
    3. setTimeout() 和 setInterval() 第一个参数的回调函数中的 this 默认指向 window

      setTimeout(function () {
          console.log(this) // window
      }, 0)
      
  • 箭头函数的 this 箭头函数中的 this 指向外层函数(非箭头函数)的作用域中的 this 指向。 箭头函数的 this 指向在被定义的时候就确定了,之后永远都不会改变。即使使用 call()、apply()、bind()等方法改变 this 指向也不可以。

    所有绑定规则不适应箭头函数。

    1. 默认绑定规则(独立调用对箭头函数)无效

      function foo() {
          console.log(this) // obj
          // 箭头函数
          var test = () => {
              console.log(this) // obj
          }
          return test
      }
      var obj = {
          a: 1,
          foo: foo
      }
      obj.foo()()
      
    2. 显示绑定无效

      function foo() {
          console.log(this) // window
          var test = () => {
              console.log(this) // window
          }
          return test
      }
      var obj2 = {
          a: 2
      }
      foo().call(obj2)
      
    3. 隐式绑定无效

      var obj4 = {
          foo: () => {
              console.log(this) // window
          }
      }
      obj4.foo()
      
  • 构造函数中的 this 构造函数中的 this 是指向实例。

    function Fn() {
        console.log(this) // Fn{}
    }
    var fn = new Fn()
    
  • 原型链中的 this this 这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它所属于的对象。

call apply bind

var name = "111"
var obj1 = {
    name: "obj1",
    getName: function (a, b, c) {
        console.log("getName1", this.name)
        console.log("参数", a, b, c)
    }
}
var obj2 = {
    name: "obj2",
    getName: function () {
        console.log("getName2", this.name)
    }
}

// call执行函数,并改变this执行为函数的第一个参数
//支持多个参数
obj1.getName.call(obj2, 1, 2, 3)

// apply执行函数,并改变this执行为函数的第一个参数
//两个参数,第二个参数是一个数组
obj1.getName.apply(obj2, [1, 2, 3])

// bind改变this指向为函数的第一个参数,不会自动执行函数
// 支持多个参数
var fun1 = obj1.getName.bind(obj2, 1, 2, 3)
fun1() //手动执行
// var fun1 = obj1.getName.bind(obj2)
// fun1(1, 2, 3)

btn.onclick = handler.bind(window)
function handler() {
    console.log(this.name)
}

get/set

get 关键字将对象属性与函数进行绑定,当属性被访问时,对应函数被执行。如果在 get 方法中调用this.属性名就会无限执行 get 方法。

set 关键字将对象属性与函数进行绑定,当属性被赋值时,对应函数被执行。如果在 set 方法中给this.属性名赋值就会无限执行 set 方法。

当一个属性被定义为存取器属性时,JavaScript 会忽略它的 value 和 writable 特性,取而代之的是 set 和 get(还有 configurable 和 enumerable)特性。

set 和 get 一般一起出现,如果只定义了一个会有特殊意义:

  • 如果只有 get,表示该属性只可读,不可写

  • 如果只有 set,表示该属性只可写,不可读

原型属性写法

function Num(n) {
    this._num = n
}
Num.prototype = {
    get num() {
        console.log("get")
        return this._num
    },
    set num(n) {
        console.log(n, "set")
        this._num = n
    }
}
let nu = new Num(3)
nu.num = 34
console.log(nu.num)

对象属性写法

function Num(n) {
    let me = this
    me._num = n
    return {
        get num() {
            console.log("get")
            return me._num
        },
        set num(n) {
            console.log("set", n)
            me._num = n
        }
    }
}
let nu = new Num(3)
nu.num = 34
console.log(nu.num)

es6 写法

class Num {
    constructor(n) {
        this._num = n
    }
    get num() {
        console.log("get")
        return this._num
    }
    set num(n) {
        console.log("set", n)
        this._num = n
    }
}

let nu = new Num(3)
nu.num = 34
console.log(nu.num)

Object.defineProperty()

语法说明 Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

Object.defineProperty(obj, prop, desc)
  • obj 需要定义属性的当前对象
  • prop 需被定义或修改的属性名。
  • desc 需被定义或修改的属性的描述符。

一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过 Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性。

属性的特性以及内部属性 javascript 有三种类型的属性

  1. 命名数据属性:拥有一个确定的值的属性。这也是最常见的属性
  2. 命名访问器属性:通过 getter 和 setter 进行读取和赋值的属性
  3. 内部属性:由 JavaScript 引擎内部使用的属性,不能通过 JavaScript 代码直接访问到,不过可以通过一些方法间接的读取和设置。比如,每个对象都有一个内部属性[[Prototype]],你不能直接访问这个属性,但可以通过 Object.getPrototypeOf()方法间接的读取到它的值。虽然内部属性通常用一个双中括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性

属性描述符

通过 Object.defineProperty()为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符。

数据描述符 --特有的两个属性(value,writable)

value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined writable: 仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false

let Person = {}
Object.defineProperty(Person, "name", {
    value: "jack",
    writable: true // 是否可以改变
})
let person = {}
Object.defineProperty(person, "name", {
    value: "Jack"
})
person.name = "rose"
// writable默认是false, 不能改变属性的值
console.log(person.name) //"Jack"

存取描述符 --是由一对 getter、setter 函数功能来描述的属性

  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为 undefined。
let person = {}
let temp = null
Object.defineProperty(person, "name", {
    get: function () {
        return temp
    },
    set: function (val) {
        temp = val
    }
})

person.name = 123
console.log(person.name) //123
console.log(temp) //123

数据描述符和存取描述符均具有以下描述符

  • configurable: 仅当该属性的 configurable 为 true 时,该属性才能够配置,也能够被删除。默认为 false
  • enumerable: 仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false

configrable

configurable: false 时,不能删除当前属性,且不能重新配置当前属性的描述符(有一个小小的意外:可以把 writable 的状态由 true 改为 false,但是无法由 false 改为 true),但是在 writable: true 的情况下,可以改变 value 的值

configurable:false 不能删除属性:

"use strict"
let Person = {}
Object.defineProperty(Person, "name", {
    value: "jack",
    configurable: false,
    writable: true,
    enumerable: true
})
delete Person.name
//Cannot delete property 'name' of #<Object>

configurable:false 不能重新定义属性:

let Person = {}
Object.defineProperty(Person, "name", {
    value: "jack",
    configurable: false
})
Object.defineProperty(Person, "name", {
    value: "rose"
    //Cannot redefine property: name
})

在 configurable:true 但 writable 为 false 的情况下可以通过属性定义的形式可以修改 name 的值:

let Person = {}
Object.defineProperty(Person, "name", {
    value: "jack",
    configurable: true,
    writable: false
})
Object.defineProperty(Person, "name", {
    value: "rose"
})
//通过属性定义的形式可以修改name的属性值
console.log(Person.name) //rose
//通过赋值的形式,不可以修改,因为writable为false
Person.name = "tom"
console.log(Person.name) //rose

在 configurable:false 但 writable 为 true 的情况下可以修改 value 值:

let Person = {}
Object.defineProperty(Person, "name", {
    value: "jack",
    configurable: false,
    writable: true
})
Object.defineProperty(Person, "name", {
    value: "rose"
})
//通过属性定义的形式可以修改name的属性值
console.log(Person.name) //rose

enumerable

let Person = {}
Object.defineProperty(Person, "name", {
    value: "Jack",
    enumerable: false
})
Person.gender = "male"
Object.defineProperty(Person, "age", {
    value: "26",
    enumerable: true
})
console.log(Object.keys(Person)) //[ 'gender', 'age' ]
for (let k in Person) {
    console.log(k)
} // gender, age

注意:以下二种区别

let Person = {}
Person.gender = "male"
//等价于
Object.defineProperty(Person, "gender", {
    value: "male",
    configurable: true,
    writable: true,
    enumerable: true
})
Object.defineProperty(Person, "age", {
    value: "26"
})
//等价于
Object.defineProperty(Person, "age", {
    value: "26",
    configurable: false,
    writable: false,
    enumerable: false
})

不变性

结合 writable: false 和 configurable: false 就可以创建一个真正的常量属性(不可修改,不可重新定义或者删除)

禁止扩展 禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions(...)

var Person = {
    name: "Jack"
}
Object.preventExtensions(Person)

//可以修改
Person.name = 123
console.log(Person.name) //123

//仍然可以进行配置
Object.defineProperty(Person, "name", {
    value: "rose",
    writable: false,
    configurable: true
})
console.log(Person.name) //rose

//不能进行扩展
Person.gender = "male"
console.log(Person.gender) //undefined

在非严格模式下,创建属性 gender 会静默失败,在严格模式下,将会抛出异常。

密封 Object.seal()会创建一个密封的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(...)并把所有现有属性标记为 configurable:false。

var Person = {
    name: "Jack"
}
Object.seal(Person)

Person.gender = "male"
//不能扩展属性
console.log(Person.gender) //undefined

//再次验证
console.log(Object.keys(Person)) //["name"]

//不能再次配置属性
Object.defineProperty(Person, "name", {
    //Cannot redefine property: name
    value: "rose",
    configurable: true
})

//可以修改
Object.defineProperty(Person, "name", {
    value: "rose"
})
Person.name = "Jack"

所以, 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以改属性的值)

冻结 Object.freeze()会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(),并把所有现有属性标记为 writable: false,这样就无法修改它们的值。

var Person = {
    name: "Jack"
}
Object.freeze(Person)

//不能扩展属性
Person.gender = "male"
console.log(Person.gender) //undefined

//不可修改已有属性的值
Person.name = "Tom"
console.log(Person.name) //Jack

//不能再次配置属性
Object.defineProperty(Person, "name", {
    value: "rose",
    configurable: true
}) //Cannot redefine property: name

这个对象引用的其他对象是不受影响的

Object.create()

创建一个拥有指定原型和若干个指定属性的新对象,使用现有的对象来提供新创建的对象的_proto_

语法 Object.create(proto, descriptors)

第一个参数:新创建对象的原型对象,必须为 null 或者原始包装对象,否则会抛出异常

第二个参数:可选参数,需要是一个对象,将为新创建的对象添加指定的属性值和对应的属性描述符

let obj = {
    name: "sun",
    age: 24
}

// 可以看到name属性其实不是newObj自身原有的,而是继承而来
let newObj = Object.create(obj, {
    age: {
        value: 12,
        writable: true,
        configurable: true,
        enumerable: true
    }
}) // {age: 12}
console.log(newObj.name) // sun

// obj就是newObj的原型对象
newObj.__proto__ === obj // true

// 使用Object.getPrototypeOf()来获取指定对象的原型对象
Object.getPrototypeOf(newObj) // {name: "sun", age: 24} // obj

Object.assign()

用于对象之间的合并

语法: Object.assign(target, source1, source2, ...)

object.assign 方法的第一个参数是目标(多个对象中可枚举属性都保存到第一个里面)对象,后面的参数都是源对象

注意: 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

var target1 = {
    a: 1,
    b: 1
}
var source3 = {
    b: 2,
    c: 2
}
var source4 = {
    c: 3
}
Object.assign(target1, source3, source4)
console.log(target1) //{a:1,b:2,c:3}

如果只有一个参数,Object.assign 会直接返回该参数

var obj = {
    a: 1
}
console.log(Object.assign(obj) === obj)
//true 如果该参数不是对象,则会先转成对象,然后返回
console.log(typeof Object.assign(2)) //Object

由于 undefined 和 null 无法转成对象,所以如果他们作为参数,就会报错

如果是非对象参数出现在源对象的位置(即非首参数),那么处理规则会有所不同,首先,这些参数都会转成对象,如果无法转成对象,就会跳过,这意味着,如果 undefined 和 null 不在首参数,就不会报错

其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式拷贝入目标对象,因为只有字符串的包装对象,会产生可枚举属性。

const v1 = "abc"
const v2 = true
const v3 = 10

const obj = Object.assign({}, v1, v2, v3)
console.log(obj)
// { "0": "a", "1": "b", "2": "c" }

注意点:

  1. 浅拷贝 Object.assign 方法实行的是浅拷贝

  2. 数组的处理 Object.assign 可以用来处理数组,但是会把数组视为对象

    console.log(Object.assign([1, 2, 3, 4], [4, 6])) //[4, 6, 3, 4]
    
    const obj = Object.assign({}, "abc", [1, 2])
    console.log(obj) //{0: 1, 1: 2, 2: 'c'}
    
  3. 取值函数的处理 Object.assign 只能进行值的复制,如果要复制的值是一个取值函数, 那么将求值后再复制。

    const sources = {
        get foo() {
            return 1
        }
    }
    const target1 = {}
    console.log(Object.assign(target1, sources)) //{foo: 1}
    

面向对象

  • 面向对象不是语法,是一个思想,是一种编程模式
  • 面向过程:关注着过程的编程模式
  • 面向对象:关注着对象的编程模式

面向对象基本特征

  1. 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  2. 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
  3. 多态:对象的多功能,多方法,一个方法多种表现形式。

对象实例化方式

工厂模式

function createCar(color, wheel) {
    //createCar工厂
    var obj = new Object() //或obj = {} 原材料阶段
    obj.color = color //加工
    obj.wheel = wheel //加工
    return obj //输出产品
}
//实例化
var cat1 = createCar("红色", "4")
var cat2 = createCar("蓝色", "4")

alert(cat1.color) //红色

构造函数模式:加 new 执行的函数构造内部变化:自动生成一个对象,this 指向这个新创建的对象,函数自动返回这个新创建的对象

function CreateCar(color, wheel) {
    //构造函数首字母大写
    //不需要自己创建对象了
    this.color = color //添加属性,this指向构造函数的实例对象
    this.wheel = wheel //添加属性

    //不需要自己return了
}

//实例化
var cat1 = new CreateCar("红色", "4")
var cat2 = new CreateCar("蓝色", "4")
alert(cat1.color) //红色

构造函数注意事项

  • 此时 CreateCar 称之为构造函数,也可以称之类,构造函数就是类 。
  • cat1,cat2 均为 CreateCar 的实例对象。
  • CreateCar 构造函数中 this 指向 CreateCar 实例对象。
  • 必须使用 new 。
  • 构造函数首字母大写,这是规范。

构造函数的问题 存在一个浪费内存的问题。如果现在为其再添加一个方法 showWheel。对于每一个实例对象,showWheel 都是一模一样的内容,每一次生成一个实例,都必须生成重复的内容

function CreateCar(color, wheel) {
    this.color = color
    this.wheel = wheel
    this.showWheel = function () {
        //添加一个新方法
        alert(this.wheel)
    }
}

//还是采用同样的方法,生成实例:
var cat1 = new CreateCar("红色", "4")
var cat2 = new CreateCar("蓝色", "4")

alert(cat1.showWheel == cat2.showWheel) //false

###  class 类

ES6 提供了 Class(语法糖)这个概念,作为对象的模板,通过 class 关键字,可以定义类

class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }

    name

    age = 0 //实例身上

    toString() {
        return "(" + this.x + ", " + this.y + ")"
    }
}

class 里面定义的方法,其实都是定义在构造函数的原型上面实现实例共享,属性定义在构造函数中,所以 ES6 中的类完全可以看作构造函数的另一种写法

原型

JavaScript 是基于原型的,当创建一个函数的时候,系统就会自动给函数分配一个 prototype(原型) 属性,这个属性是一个指针,默认指向一个空对象,可以用来存储让所有实例共享的属性和方法。

1.png

  • 每一个构造函数都拥有一个 prototype 属性,这个属性指向一个对象,也就是原型对象

  • 原型对象默认拥有一个 constructor 属性,指向它的那个构造函数

  • 每个对象都拥有一个隐藏的属性 __proto__,指向它的原型对象

  • constructor 会被实例继承。它的作用就是指名某个实例对象是由哪个构造函数产生的。

    function Person() {}
    
    var person = new Person()
    
    person.__proto__ === Person.prototype // true
    
    Person.prototype.constructor === Person // true
    
    person.constructor === Person.prototype.constructor //true
    

原型特点

function Person() {}
Person.prototype.name = "tt"
Person.prototype.age = 18
Person.prototype.sayHi = function () {
    alert("Hi")
}
var person = new Person()
person.name = "oo"
person.name // oo
person.age // 18
perosn.sayHi() // Hi

实例可以共享原型上面的属性和方法 实例自身的属性会屏蔽原型上面的同名属性,实例上面没有的属性会去原型上面找。

### 原型链

JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链。

2.png

所有原型链的终点都是 Object 函数的 prototype 属性 Object.prototype 指向的原型对象同样拥有原型,不过它的原型是 null ,而 null 则没有原型

原型链的问题

function Person() {}
Person.prototype.arr = [1, 2, 3, 4]

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

person1.arr.push(5)
person2.arr // [1, 2, 3, 4, 5]

当原型上面的属性是一个引用类型的值时,我们通过其中某一个实例对原型属性的更改,结果会反映在所有实例上面。

对象和函数的关系

3.png

对象是由函数构造出来的。

Object 是 Function 的一个实例。

Object.constructor == Function //true

函数是 Function 的实例,但不是 Object 的实例。

function fn() {}
fn.constructor == Function //true
fn.constructor == Object //false

{} 与 Object 的关系。

var obj = {}
obj.constructor === Object //true

继承

继承的本质就是复制,即重写原型对象,代之以一个新类型的实例。

  1. 原型链继承

    原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。

    function SuperType() {
        this.colors = ["red", "blue", "green"]
    }
    function SubType() {}
    
    // 这里是关键,创建 SuperType 的实例,并将该实例赋值给 SubType.prototype
    SubType.prototype = new SuperType()
    
    // 重写 SubType.prototype 的 constructor 属性,指向自己的构造函数 SubType
    SubType.prototype.constructor = SubType
    
    var instance1 = new SubType()
    instance1.colors.push("black")
    alert(instance1.colors) //"red,blue,green,black"
    
    var instance2 = new SubType()
    alert(instance2.colors) //"red,blue,green,black"
    
  2. 借用构造函数继承 使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

    function SuperType() {
        this.color = ["red", "green", "blue"]
    }
    function SubType() {
        //继承自 SuperType
        SuperType.call(this)
    }
    var instance1 = new SubType()
    instance1.color.push("black")
    alert(instance1.color) //"red,green,blue,black"
    
    var instance2 = new SubType()
    alert(instance2.color) //"red,green,blue"
    

    核心代码是 SuperType.call(this),创建子类实例时调用 SuperType 构造函数,于是 SubType 的每个实例都会将 SuperType 中的属性复制一份。

    缺点:

    • 只能继承父类的实例属性和方法,不能继承原型属性/方法
    • 无法实现复用,每个子类都有父类实例函数的副本,影响性能
  3. 组合继承 组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。

    function SuperType(name) {
        this.name = name
        this.colors = ["red", "blue", "green"]
    }
    SuperType.prototype.sayName = function () {
        alert(this.name)
    }
    
    function SubType(name, age) {
        // 继承属性
        // 第二次调用 SuperType()
        SuperType.call(this, name)
        this.age = age
    }
    
    // 继承方法
    // 构建原型链
    // 第一次调用 SuperType()
    SubType.prototype = new SuperType()
    // 重写 SubType.prototype 的 constructor 属性,指向自己的构造函数 SubType
    SubType.prototype.constructor = SubType
    SubType.prototype.sayAge = function () {
        alert(this.age)
    }
    
    var instance1 = new SubType("Nicholas", 29)
    instance1.colors.push("black")
    alert(instance1.colors) //"red,blue,green,black"
    instance1.sayName() //"Nicholas";
    instance1.sayAge() //29
    
    var instance2 = new SubType("Greg", 27)
    alert(instance2.colors) //"red,blue,green"
    instance2.sayName() //"Greg";
    instance2.sayAge() //27
    

    缺点:

    第一次调用 SuperType():给 SubType.prototype 写入两个属性 name,color。 第二次调用 SubType():给 instance1 写入两个属性 name,color。

    实例对象 instance1 上的两个属性就屏蔽了其原型对象 SuperType.prototype 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

  4. 原型式继承 利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

    function object(obj) {
        function F() {}
        F.prototype = obj
        return new F()
    }
    

    object()对传入其中的对象执行了一次浅复制,将构造函数 F 的原型直接指向传入的对象。

    var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    }
    
    var anotherPerson = object(person)
    anotherPerson.name = "Greg"
    anotherPerson.friends.push("Rob")
    
    var yetAnotherPerson = object(person)
    yetAnotherPerson.name = "Linda"
    yetAnotherPerson.friends.push("Barbie")
    
    alert(person.friends) //"Shelby,Court,Van,Rob,Barbie"
    

    缺点:

    原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。 无法传递参数

    另外,ES5 中存在 Object.create()的方法,能够代替上面的 object 方法。

  5. 寄生式继承 核心:在原型式继承的基础上,增强对象,返回构造函数

    function createAnother(original) {
        var clone = object(original) // 通过调用 object() 函数创建一个新对象
        clone.sayHi = function () {
            // 以某种方式来增强对象
            alert("hi")
        }
        return clone // 返回这个对象
    }
    

    函数的主要作用是为构造函数新增属性和方法,以增强函数

    var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    }
    var anotherPerson = createAnother(person)
    anotherPerson.sayHi() //"hi"
    

    缺点(同原型式继承): 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。 无法传递参数

  6. 寄生组合式继承 结合借用构造函数传递参数和寄生模式实现继承

    function inheritPrototype(subType, superType) {
        var prototype = Object.create(superType.prototype) // 创建对象,创建父类原型的一个副本
        prototype.constructor = subType // 增强对象,弥补因重写原型而失去的默认的 constructor 属性
        subType.prototype = prototype // 指定对象,将新创建的对象赋值给子类的原型
    }
    
    // 父类初始化实例属性和原型属性
    function SuperType(name) {
        this.name = name
        this.colors = ["red", "blue", "green"]
    }
    SuperType.prototype.sayName = function () {
        alert(this.name)
    }
    
    // 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
    function SubType(name, age) {
        SuperType.call(this, name)
        this.age = age
    }
    
    // 将父类原型指向子类
    inheritPrototype(SubType, SuperType)
    
    // 新增子类原型属性
    SubType.prototype.sayAge = function () {
        alert(this.age)
    }
    
    var instance1 = new SubType("xyc", 23)
    var instance2 = new SubType("lxy", 23)
    
    instance1.colors.push("2") // ["red", "blue", "green", "2"]
    instance1.colors.push("3") // ["red", "blue", "green", "3"]
    

    这个例子的高效率体现在它只调用了一次 SuperType  构造函数,并且因此避免了在 SubType.prototype  上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用 instanceof  和 isPrototypeOf() 这是最成熟的方法,也是现在库实现的方法

  7. 混入方式继承多个对象

    function MyClass() {
        SuperClass.call(this)
        OtherSuperClass.call(this)
    }
    
    // 继承一个类
    MyClass.prototype = Object.create(SuperClass.prototype)
    // 混合其它
    Object.assign(MyClass.prototype, OtherSuperClass.prototype)
    // 重新指定 constructor
    MyClass.prototype.constructor = MyClass
    
    MyClass.prototype.myMethod = function () {
        // do something
    }
    

    Object.assign 会把 OtherSuperClass 原型上的函数拷贝到 MyClass 原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

  8. ES6 类继承 extends extends 关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中 constructor 表示构造函数,一个类中只能有一个构造函数,有多个会报出 SyntaxError 错误,如果没有显式指定构造方法,则会添加默认的 constructor 方法。

    class Rectangle {
        // constructor
        constructor(height, width) {
            this.height = height
            this.width = width
        }
    
        // Getter
        get area() {
            return this.calcArea()
        }
    
        // Method
        calcArea() {
            return this.height * this.width
        }
    }
    
    const rectangle = new Rectangle(10, 20)
    console.log(rectangle.area)
    // 输出 200
    
    // 继承
    class Square extends Rectangle {
        constructor(length) {
            super(length, length)
    
            // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
            this.name = "Square"
        }
    
        get area() {
            return this.height * this.width
        }
    }
    
    const square = new Square(10)
    console.log(square.area)
    // 输出 100
    

    extends 继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

    function _inherits(subType, superType) {
        // 创建对象,创建父类原型的一个副本
        // 增强对象,弥补因重写原型而失去的默认的constructor 属性
        // 指定对象,将新创建的对象赋值给子类的原型
        subType.prototype = Object.create(superType && superType.prototype, {
            constructor: {
                value: subType,
                enumerable: false,
                writable: true,
                configurable: true
            }
        })
    
        if (superType) {
            Object.setPrototypeOf
                ? Object.setPrototypeOf(subType, superType)
                : (subType.__proto__ = superType)
        }
    }
    

总结 1、函数声明和类声明的区别 函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个 ReferenceError。

let p = new Rectangle()
// ReferenceError

class Rectangle {}

2、ES5 继承和 ES6 继承的区别

ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.call(this))

ES6 的继承有所不同,实质上是先创建父类的实例对象 this,然后再用子类的构造函数修改 this。因为子类没有自己的 this 对象,所以必须先调用父类的 super()方法,否则新建实例报错。

多态

同一个方法,面对不同的对象有不同的表现形式就叫做多态。

var makeSound = function (animal) {
    animal.sound()
}

var Duck = function () {}
Duck.prototype.sound = function () {
    console.log("嘎嘎嘎")
}
var Chicken = function () {}
Chicken.prototype.sound = function () {
    console.log("咯咯咯")
}

makeSound(new Chicken())
makeSound(new Duck())

super

super 作为函数调用

在子类继承父类中,如果 super 作为函数调用,只能写在子类的构造函数(constructor)里面,代表的是父类的构造函数

class A {
    constructor() {}
}

class B extends A {
    constructor() {
        super() // 调用super()
    }
}

在上面的代码中,子类 B 的构造函数之中的 super(),它代表调用父类的构造函数。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。 子类没有定义 constructor 方法时,super 方法会被默认添加。

注意 super 虽然代表了父类 A 的构造函数,但是 super 内部的 this 指的是 B 的实例

换一种理解是:在执行 super 时,A 把 constructor 方法给了 B,此时 B 有了 A 的功能,但是执行的是 B 的内容,也就是 es5 的 A.call(this)

super 作为对象使用

普通方法/构造函数中使用:super 指向父类的 prototype

  • 在子类普通方法/构造函数中 super 作为对象使用时, 通过 super 调用父类的 prototype 的方法时(当 prototype 没有这个方法时会通过原型链寻找), 方法内部的 this 指向当前子类的实例。
  • 通过 super 访问某个属性时 super 指向父类的 prototype,对某个属性赋值时,super 就是 this,是给子类实例的属性赋值
  • 由于 super 指向父类的 prototype 对象, 所以定义在父类实例上的方法或属性, 是无法通过 super 调用的
class A {
    constructor() {
        this.name = "itclanCoder"
    }
}
A.prototype.name = 123

class B extends A {
    constructor() {
        super()
        console.log(this.name) // itclanCoder
        this.name = "itclan"
        super.name = "川川"
        console.log(super.name) // 123
        console.log(this.name) // 川川
    }
}

let b = new B()

静态方法中使用:super 指向父类

  • 在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类而不是子类的实例
  • 在子类的静态方法中通过 super 给属性赋值时,是给子类的属性赋值,访问时是指向父类属性

静态方法/函数

  1. 类的静态方法
function BaseClass() {}
// 类添加add函数
BaseClass.add = function () {
    console.log("BaseClass add()方法被调用")
}

BaseClass.add() //BaseClass add()方法被调用

var instance = new BaseClass()
// 实例不能调用类方法(即类的静态方法)
//instance.add();
  1. 类的静态属性
function BaseClass(params) {}

// 类添加静态变量 nameTest
BaseClass.nameTest = "jadeshu"

console.log(BaseClass.nameTest) // jadeshu

var instance = new BaseClass()
// 实例不能调用类的静态成员变量)
console.log(instance.nameTest) // undefined
  1. ES6

ES6 中父类的静态方法/属性可以被子类继承

class C {
    static age = 1
    static add() {
        console.log(123)
    }
}
C.sex = function () {
    console.log("男")
}
C.num = 100

class D extends C {}
D.add() //123
console.log(D.num) //100

私有方法/属性

JavaScript 之前没有真正的私有属性和方法。这种功能的缺乏导致之前都通过约定俗成的下划线前缀,来表示该属性和方法为私有属性或私有方法:

function User(name) {
    this._id = "xyz"
    this.name = name
}

User.prototype.getUserId = function () {
    return this._id
}

User.prototype._destroy = function () {
    this._id = null
}

const user = new User("Todd Motto")
user._id // xyz
user.getUserId() // xyz
user._destroy()
user.getUserId() // null

即使 this._id 与 User.prototype._destroy 被认为是私有的(下划线),但实际上任何地方都可以像调用公共属性方法来调用它,因为它们是 User 对象的一部分。

Class 私有属性和方法 通过#号关键字,将一个 Class 类的属性设置为私有属性

class User {
    //必须在类本身上声明
    #id = ""
    constructor(id) {
        // 赋值
        this.#id = id
    }
    getUserId() {
        console.log(this.#id)
        return this.#id
    }
    static get #num() {
        return 123
    }
    static getNum() {
        console.log(this.#num)
    }
}
User.getNum() //123

let user = new User("李白")
user.getUserId() //"李白"

需要注意的是#id 属性名称为 id,而不是#id。

instanceof

用来检测 constructor.prototype 是否存在于参数 object 的原型链上。instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型。

语法:object instanceof constructor object:某个实例对象 constructor:某个构造函数

function Foo() {}
function Bar() {}
Bar.prototype = new Foo()

let obj = new Bar()
obj instanceof Bar //true
obj instanceof Foo //true
var str = "str"
console.log(str instanceof String) //false
console.log(typeof str) //string

var strobj = new String("bbb")
console.log(strobj instanceof String) //true

hasOwnProperty()

通过使用 hasOwnProperty 可以确定访问的属性是来自于实例还是原型对象

function Person() {}
Person.prototype = {
    name: "tt"
}
var person = new Person()
person.age = 15

person.hasOwnProperty("age") // true
person.hasOwnProperty("name") // false

Object.setPrototypeOf()

Object.setPrototypeOf 方法的作用与 __proto__ 相同,用来设置一个对象的原型对象,返回参数对象本身。

let proto = {}
let obj = { x: 10 }
Object.setPrototypeOf(obj, proto)

proto.y = 20
proto.z = 40

obj.x // 10
obj.y // 20
obj.z // 40

Object.getPrototypeOf()

用于读取一个对象的原型对象。

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf("foo") === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

AJAX

Ajax(Asynchronous Javascript And XML),即是异步的 JavaScript 和 XML,Ajax 其实就是浏览器与服务器之间的一种异步通信方式

  1. 不需要插件的支持,原生 js 就可以使用
  2. 用户体验好(不需要刷新页面就可以更新数据)
  3. 减轻服务端和带宽的负担
  4. 缺点:搜索引擎的支持度不够,因为数据都不在页面上,搜索引擎搜索不到

AJAX 的使用

  • 在 js 中有内置的构造函数来创建 ajax 对象
  • 创建 ajax 对象以后,我们就使用 ajax 对象的方法去发送请求和接受响应
Ajax状态码 状态
0 (未初始化)未启动
1 (启动)已经调用 open(),但尚未调用 send()
2 (发送)发送状态,已经调用 send(),但尚未接收到响应
3 (接收)已经接收到部分响应数据
4 (完成)已经接收到全部响应数据,而且已经可以在浏览器中使用了
//1.创建XHR new XMLHttpRequest()
var xhr = new XMLHttpRequest()
console.log(xhr)

//2.配置open(请求方式,请求地址,是否异步)
xhr.open("GET", "http://localhost:5500/136-ajax/1.txt") //第三个参数 true表示异步请求 false表示同步请求

//3. send
xhr.send()

//4.接受数据,注册一个事件
//异步执行放send前面后面都行
xhr.onreadystatechange = function () {
    //readyStateChange事件是专门用来监听xhr对象的Ajax状态码,只要readyState(也就是Ajax状态码)发生了变化,就会触发这个事件

    // console.log (xhr.readyState)
    if (xhr.readyState == 4 && xhr.status == 200) {
        console.log("数据解析完成", xhr.responseText)
        document.write(xhr.responseText)
    } else if (xhr.readystate == 4 && xhr.status === 404) {
        console.error("没有找到这个页面")
        // location.href ="404.html"
    }
}
//或者
xhr.onload = function () {
    // console.log(xhr.responseText)
    if (xhr.status === 200) {
        document.write(xhr.responseText)
    } else if (xhr.status == 404) {
        console.error("没有找到这个页面")
        // location.href = "404.html"
    }
}

需要判断 HTTP 的状态码,判断 xhr 对象的 status 属性值是否在 200 到 300 之间(200-299 用于表示请求成功)

请求方式

test.json

{
    "users": [
        {
            "id": 1,
            "username": "kerwin",
            "password": "123456"
        },
        {
            "id": 2,
            "username": "tiechui",
            "password": "123456"
        }
    ],
    "list": ["1111", "2222", "3333"]
}

get 偏向获取数据

myget.onclick = function () {
    var xhr = new XMLHttpRequest()
    xhr.open("GET", "http://localhost:3000/users")
    //xhr.open("GET", "http://localhost:3000/users?username=kerwin&password=123")
    xhr.onload = function () {
        if (xhr.status === 200) {
            console.log(JSON.parse(xhr.responseText))
        }
    }
    xhr.send()
}

post 偏向提交数据

mypost.onclick = function () {
    var xhr = new XMLHttpRequest()
    xhr.open("POST", "http://localhost:3000/users")
    xhr.onload = function () {
        if (/^2\d{2}$/.test(xhr.status)) {
            console.log(JSON.parse(xhr.responseText))
        }
    }
    //提交信息
    //post name=kerwin&age=100
    //xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded") //name=kerwin&age=100
    //xhr.send(username=shanzhen&password=456)
    xhr.setRequestHeader("Content-Type", "application/json")
    xhr.send(
        JSON.stringify({
            username: "ximen",
            password: "789"
        })
    )
}

put 偏向更新(全部)

myput.onclick = function () {
    var xhr = new XMLHttpRequest()
    xhr.open("PUT", "http://localhost:3000/users/1")
    xhr.onload = function () {
        if (/^2\d{2|$/.test(xhr.status)) {
            console.log(JSON.parse(xhr.responseText))
        }
    }
    xhr.setRequestHeader("Content-Type", " application/json")
    xhr.send(
        JSON.stringify({
            username: "ximen11111111"
        })
    )
}

patch 偏向部分修改

mypatch.onclick = function () {
    var xhr = new XMLHttpRequest()
    xhr.open("PATCH", "http://localhost:3000/users/2")
    xhr.onload = function () {
        if (/^2\d{2|$/.test(xhr.status)) {
            console.log(JSON.parse(xhr.responseText))
        }
    }
    xhr.setRequestHeader("Content-Type", "application/json")
    xhr.send(
        JSON.stringify({
            username: "xiaoming11111111"
        })
    )
}

delete 偏向删除信息

mydelete.onclick = function () {
    var xhr = new XMLHttpRequest()
    xhr.open("DELETE", "http://localhost:3000/users/1")
    xhr.onload = function () {
        if (xhr.status === 200) {
            console.log(JSON.parse(xhr.responseText))
        }
    }
    xhr.send()
}

header options connnect

封装

function queryString(obj) {
    let data = ""
    for (let key in obj) data += `${key}=${obj[key]}&`
    return data.slice(0, -1)
}
function ajax(options) {
    let defaultOptions = {
        method: "GET",
        url: "",
        data: {},
        async: true,
        headers: { "Content-Type": "application/json" },
        success: function () {},
        error: function () {}
    }

    let { method, url, data, async, headers, success, error } = {
        ...defaultOptions,
        ...options
    }
    if (/^get$/i.test(method) && data) {
        url += "?" + queryString(data)
    }
    if (typeof data === "object" && headers["Content-Type"]?.indexOf("json") > -1) {
        data = JSON.stringify(data)
    } else {
        data = queryString(data)
    }
    const xhr = new XMLHttpRequest()
    xhr.open(method, url, async)
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (!/^2\d{2}$/.test(xhr.status)) {
                error(`错误状态码:${xhr.status}`)
                return
            }
            try {
                success(JSON.parse(xhr.responseText))
            } catch (err) {
                error("解析错误")
            }
        }
    }
    for (let key in headers) xhr.setRequestHeader(key, headers[key])
    if (/^get$/i.test(method)) {
        xhr.send()
    } else {
        xhr.send(data)
    }
}
export default ajax
ajax({
    url: "http://localhost:3000/users",
    method: "GET",
    async: true,
    data: {
        username: "kerwin",
        password: "123"
    },
    headers: {},
    success: function (res) {
        console.log(res)
    },
    error: function (err) {
        console.log(err)
    }
})
//promise ajax
function pajax(options) {
    return new Promise((resolve, reject) => {
        ajax({
            ...options,
            success(res) {
                resolve(res)
            },
            error(err) {
                reject(err)
            }
        })
    })
}

fetch

优点:

  • 使用更方便。fetch 是浏览器原生支持的请求方法,可以直接在浏览器中使用,也可以在代码中随时使用,而不需要像 axios 一样引入第三方包
  • 脱离了浏览器的 XHR,是 ES 规范里新的实现方式
  • 是基于 promise 的异步请求

get

myget.onclick = function () {
    var username = "kerwin"
    fetch(`http://localhost:3000/users111?username=${username}`)
        .then(res => {
            console.log(res)
            if (res.ok) {
                return res.json()
            } else {
                //拒绝
                return Promise.reject({
                    a: 1,
                    status: res.status,
                    statusText: res.statusText
                })
            }
        })
        .then(res => {
            console.log("success", res)
        })
        .catch(err => {
            console.log("error", err)
        })
}
// res.json()返回结果和 JSON.parse(responseText) 一样
// res.text() 将返回体处理成字符串类型

post

fetch("http://localhost:3000/users", {
    method: "POST",
    headers: {
        "content-type": "application/x-www-form-urlencoded"
    },
    body: "username=tiechui&password=123"
})
    .then(res => res.json())
    .then(res => {
        console.log(res)
    })

fetch("http://localhost:3000/users", {
    method: "POST",
    headers: {
        "content-type": "application/json"
    },
    body: JSON.stringify({
        username: "shanzhen",
        password: "234"
    })
})
    .then(res => res.json())
    .then(res => {
        console.log(res)
    })

delete

fetch("http://localhost:3000/users/2", {
    method: "delete"
})
    .then(res => res.json())
    .then(res => {
        console.log(res)
    })

Promise

  1. 抽象表达:

    • Promise 是一门新的技术(ES6 规范)
    • Promise 是 JS 中进行异步编程的新解决方案

    备注:旧方案是单纯使用回调函数

  2. 具体表达:

    • 从语法上来说:Promise 是一个构造函数
    • 从功能上来说:promise 对象用来封装一个异步操作并可以获取其成功/ 失败的结果值

异步编程

//fs文件操作
require("fs").readFile("./index.html", (err, data) => {})

//数据库操作

//AJAX
$.get("/server", data => {})

//定时器
setTimeout(() => {}, 2000)

优点:

  • 指定回调函数的方式更加灵活
    1. 旧的:必须在启动异步任务前指定
    2. promise: 启动异步任务=>返回 promise 对象=>给 promise 对象绑定回调函 数(甚至可以在异步任务结束后指定/多个)
  • 支持链式调用,可以解决回调地狱问题
    1. 什么是回调地狱 回调函数嵌套调用,外回调函数异步执行的结果是嵌套的回调执行的条件
    2. 回调地狱的缺点
      • 不便于阅读
      • 不便于异常处理
    3. 解决方案 promise 链式调用

Promise 的状态 实例对象中的一个属性「PromiseState」

  • pending:未决定的
  • resolved / fullfilled:成功
  • rejected:失败

状态改变

  1. pending 变为 resolved
  2. pending 变为 rejected

说明:只有这 2 种,且一个 promise 对象只能改变一次,无论变为成功还是失败,都会有一个结果数据

Promise 对象的值 实例对象中的另一个属性「PromiseResult」,保存着异步任务「成功/失败」的结果(resolve/reject)

基本流程

1.png

使用

  1. Promise 构造函数: Promise(excutor){}

    • executor 函数:执行器(resolve, reject)=> {}
    • resolve 函数:内部定义成功时我们调用的函数 value=> {}
    • reject 函数:内部定义失败时我们调用的函数 reason=> {}

    说明:executor 会在 Promise 内部立即同步调用,异步操作在执行器中执行

  2. Promise.prototype.then 方法: (onResolved, onRejected)=> {}

    • onResolved 函数:成功的回调函数(value) => {}
    • onRejected 函数:失败的回调函数(reason) => {}

    说明:指定用于得到成功 result 的成功回调和用于得到失败 error 的失败回调 返回一个新的 promise 对象

  3. Promise.prototype.catch 方法: (onRejected) => {}

    • onRejected 函数:失败的回调函数(reason)=> {}
  4. Promise.resolve(value) 方法:

    • value: 成功的数据或 promise 对象

    说明:返回一个成功/失败的 promise 对象

    //如果传入的参数为非Promise 类型的对象,则返回的结果为成功的promise对象
    let p1 = Promise.resolve(521)
    
    //如果传入的参数为 Promise对象,则原封不动地返回
    let p2 = Promise.resolve(
        new Promise((resolve, reject) => {
            // resolve('OK');
            reject("Error")
        })
    )
    
    console.log(p2)
    p2.catch(reason => {
        console.log(reason) //Error
    })
    
  5. Promise.reject(reason) 方法:

    • reason:失败的原因

    说明:返回一个失败的 promise 对象

    // let p = Promise.reject(521);
    
    //如果传入的参数为 Promise对象,则失败的结果为该Promise对象
    let p3 = Promise.reject(
        new Promise((resolve, reject) => {
            //resolve("OK")
            reject("err")
        })
    )
    console.log(p3)
    
    p3.catch(error => {
        error.catch(err => {
            console.log(err) //err
        })
    })
    
  6. Promise.all(promises) 方法:

    • promises: 包含 n 个 promise 的数组

    说明:用于并行执行一组异步操作,当所有的异步操作都完成时,Promise.all 会返回一个新的 promise,只有所有的 promise 都成功才成功,只要有一个失败了就直接失败。

    let p1 = new Promise((resolve, reject) => {
        resolve("OK")
    })
    let p2 = Promise.resolve("Success")
    let p3 = Promise.resolve("Oh Yeah")
    const result = Promise.all([p1, p2, p3])
    

    2.png

    let p1 = new Promise((resolve, reject) => {
        resolve("OK")
    })
    
    let p2 = Promise.reject(" Error")
    let p3 = Promise.resolve("Oh Yeah")
    
    const result = Promise.all([p1, p2, p3])
    

    3.png

  7. Promise.race(promises) 方法:

    • promises: 包含 n 个 promise 的数组

    说明:异步执行,返回一个新的 promise,第一个完成的 promise 的结果、状态就是最终的结果、状态

关键问题

  1. 如何改变 promise 的状态?

    • resolve(value): 如果当前是 pending 就会变为 resolved
    • reject(reason): 如果当前是 pending 就会变为 rejected
    • 抛出异常:如果当前是 pending 就会变为 rejected
  2. 一个 promise 指定多个成功/失败回调函数,都会调用吗? 当 promise 改变为对应状态时都会调用

    let p = new Promise((resolve, reject) => {
        resolve("OK")
    })
    p.then(value => {
        console.log(value)
    })
    p.then(value => {
        alert(value)
    })
    
  3. 改变 promise 状态和指定回调函数谁先谁后?

    • 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调
    //指定回调,改变状态,执行对应回调
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("OK")
        }, 1000)
    })
    
    p.then(
        value => {
            console.log(value)
        },
        reason => {}
    )
    
    • 如何先改状态再指定回调?
      • 在执行器中直接调用 resolve()/reject()
      • 延迟更长时间才调用 then()
    • 什么时候才能得到数据?
      • 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据
      • 如果先改变的状态, 那当指定回调时,回调函数就会调用,得到数据
  4. promise.then()返回的新 promise 的结果状态由什么决定?

    • 简单表达:由 then()指定的回调函数执行的结果决定
    • 详细表达:
      • 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常
      • 如果返回的是非 promise 的任意值,新 promise 变为 resolved,value 为返回的值,没有 return 默认返回 undefined
      • 如果返回的是另一个新 promise,此 promise 的结果就会成为新 promise 的结果
    new Promise((resolve, reject) => {
        reject(1)
    })
        .then(
            value => {
                console.log("成功", value)
            },
            reason => {
                console.log("失败", reason)
            }
        )
        .then(
            value => {
                console.log("成功", value)
            },
            reason => {
                console.log("失败", reason)
            }
        )
    // 打印结果
    //第一行:失败1
    //第二行:成功undefined
    
  5. 串连多个操作任务

    • promise 的 then()返回一个新的 promise,可以形成 then()的链式调用
    • 通过 then 的链式调用串连多个同步/异步任务
  6. promise 异常传透

    • 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调

    • 前面任何操作出了异常,都会传到最后失败的回调中处理

    • 使用.catch 会默认为没有指定失败回调函数的.then 指定失败回调函数为:

      reason => {
          throw reason
      }
      //注意不是return reason 而是throw reason,throw保证了返回结果为失败
      
    //catch的异常穿透是一层层传递下来的并非从失败状态直接传递到catch)
    new Promise((resolve, reject) => {
        reject(1)
    })
        .then(value => {
            console.log("成功", value)
        })
        .then(
            value => {
                console.log("成功", value)
            },
            reason => {
                console.log("失败hhh", reason)
            }
        )
        .catch(reason => {
            console.log("失败", reason)
        })
    //打印结果
    //失败hhh 1
    
  7. 中断 promise 链,当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数

    • 办法: 在回调函数中返回一个 pendding 状态的 promise 对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("OK")
        }, 1000)
    })
    p.then(value => {
        console.log(111)
        return new Promise(() => {})
    })
        .then(value => {
            console.log(222)
        })
        .catch(reason => {
            console.warn(reason)
        })
    
    //111
    

async 函数

  1. 函数的返回值为 promise 对象
  2. promise 对象的结果由 async 函数执行的返回值决定(与 then()类似)
async function main() {
    //1.如果返回值是一个非Promise类型的数据
    //  return 521;

    //2.如果返回的是一个Promise对象
    //return new Promise((resolve, reject) => {
    //    //resolve("OK")
    //    reject("Error")
    //})

    //3. 抛出异常
    throw "Oh NO"
}

let result = main()
console.log(result)

await 表达式

  1. await 右侧的表达式一般为 promise 对象,但也可以是其它的值
  2. 如果表达式是 promise 对象,await 返回的是 promise 成功的值
  3. 如果表达式是其它值,直接将此值作为 await 的返回值

注意

  1. await 必须写在 async 函数中,但 async 函数中可以没有 await
  2. 如果 await 的 promise 失败了,就会抛出异常,需要通过 try...catch 捕获处理
async function main() {
    let p = new Promise((resolve, reject) => {
        // resolve('OK');
        reject("Error")
    })

    //1.右侧为promise的情况
    // let res = await p;

    //2.右侧为其他类型的数据
    // let res2 = await 20;

    //3. 如果promise是失败的状态
    try {
        let res3 = await p
    } catch (e) {
        console.log(e) //Error
    }
}

cookie

Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串,来记录用户的某些信息,例如用户身份、喜好等,当用户下次访问网站时,网站可以通过检索这些信息来为用户展示个性化页面。它由一个名称(Name)、一个值(Value) 和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。

不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。

Cookie 的几大特性:

  • 自动发送
  • 域名独立
  • 过期时限
  • 4KB 限制

提示: 可以在浏览器端禁用 Cookie,这样一些借助 Cookie 才能完成的操作将无法进行。另外,不要在 Cookie 中存储账号、密码等敏感信息。

设置 Cookie

Name/Value由分号分隔,一个 Cookie 最多有 20 对,每个网页最多有一个 Cookie。

document.cookie = "url=http://c.biancheng.net/"

Cookie 数据中不能包含分号、逗号或空格,最好用 encodeURIComponent()对其编码。在读取 Cookie 时,使用对应的 decodeURIComponent() 函数来解析 Cookie 数据。

document.cookie = "url=" + encodeURIComponent("http://c.biancheng.net/")

max-age 属性来指定 Cookie 可以存在的时间(单位为秒),默认为 -1,即关闭浏览器后失效。

如果将 max-age 设置为一个负数,则表示该 Cookie 为临时 Cookie,关闭浏览器后就会失效。如果设置为 0,则表示删除该 Cookie。

document.cookie = "url=http://c.biancheng.net/; max-age=" + 30 * 24 * 60 * 60

也可以使用 expires 属性来指定 Cookie 失效的具体日期(GMT/UTC 格式,格林尼治时间,减 8 小时)。

var date = new Date()
date.setMinutes(date.getMinutes() + 10)
document.cookie = `url=http://c.biancheng.net/; expires=${date.toUTCString()}`

默认情况下,Cookie 可用于同一域名下的所有网页,但如果您为 Cookie 设置了 path 属性,那么 Cookie 就只能在该域名指定路径下的网页中使用,例如网站的域名为 c.biancheng.net,若 path 属性设置为/,则表示 Cookie 可在域名下的所有网页中使用,若 path 属性设置为/javascript/,则 Cookie 只可在 http://c.biancheng.net/javascript/ 下的网页中使用。

document.cookie = "url=http://c.biancheng.net/; path=/"

如果希望 Cookie 可以在指定域名下的子域名中使用,则可以通过 domain 属性来设置域名,默认情况下,Cookie 仅可在设置它的域名下使用。

document.cookie = "url=http://c.biancheng.net/; path=/; domain=.biancheng.net"

若将 domain 属性设置为.biancheng.net,则表示 Cookie 可在所有以 biancheng.net 结尾的域名下使用,注意,domain 属性值的第一个字符.不能省略。带点:任何 subdomain 都可以访问,包括父 domain。不带点:只有完全一样的域名才能访问,subdomain 不能

secure:只有属性名,没有属性值,表示 Cookie 将仅通过 HTTPS 协议传输

document.cookie = "url=http://c.biancheng.net/; path=/; domain=.biancheng.net; secure"

读取 Cookie 读取 Cookie 同样使用 document.cookie,该属性会返回一个字符串,字符串中包含除 max-age、expires、path 和 domain 等属性之外的所有 Cookie 信息,例如 url=c.biancheng.net/; course=JavaScript。

document.cookie = "url=http://c.biancheng.net/; max-age=" + 30 * 24 * 60 * 60
document.cookie = "course=JavaScript"
document.cookie = "title=cookie"
function getCookie(name) {
    var cookieArr = document.cookie.split(";")
    for (var i = 0; i < cookieArr.length; i++) {
        var cookiePair = cookieArr[i].split("=")
        if (name == cookiePair[0].trim()) {
            // 解码cookie值并返回
            return decodeURIComponent(cookiePair[1])
        }
    }
    return null
}
document.write("url = " + getCookie("url")) // 输出:url = http://c.biancheng.net/
document.write("course = " + getCookie("course")) // 输出:course = JavaScript

修改 Cookie 修改或更新 Cookie 值的唯一方法就是创建一个同名的 Cookie,来替换要修改的 Cookie。name/domain/path 都相同的时候才会覆盖,否则会创建一个新的 Cookie。

document.cookie = "url=http://c.biancheng.net/; path=/; max-age=" + 30 * 24 * 60 * 60
// 修改这个 Cookie
document.cookie = "url=http://c.biancheng.net/javascript/; path=/; max-age=" + 365 * 24 * 60 * 60

提示:若 path 属性为/,在修改时也可以省略 path 属性。

删除 Cookie 只需要重新将 Cookie 的值设置为空,并将 expires 属性设置为一个过去的日期即可

document.cookie = "url=http://c.biancheng.net/; path=/; max-age=" + 30*24*60\*60;
// 删除这个 Cookie
document.cookie = "url=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";

也可通过将 max-age 属性设置为 0 来删除 Cookie。

登录鉴权

对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:

  • 服务端渲染推荐使用 session-cookie 认证机制
  • 前后端分离推荐使用 JWT 认证机制

session-cookie

session 是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中保存标识该浏览器的信息。

  • session 是存储在 web 服务端容器里。
  • 存储的数据格式也是键值对。
  • 当访问服务器某个网页的时候,会在服务端开辟一个内存,这块内存为 session,而这个内存是跟浏览器关联在一起的,只允许当前这个 session 对应的浏览器访问。另外一台浏览器也需要记录 session 的话,就会在创建一个属于自己浏览器的 session。
  • session 可以与 redis 或数据库等结合做持久化操作,当服务器挂掉时也不会导致某些客户信息(购物车)消失。

缺点

  • 当存在多台服务器时会出现 session 同步问题
  • 很容易遭受到 Cookie 欺骗和 CRFS 攻击
  • 服务端存储压力,当很多的 session 存储到服务端时,会对服务器的存储造成压力
  • Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。

工作流程

  • 服务器在接受客户端首次访问时在服务器端创建 seesion,然后保存 seesion(可以将 seesion 保存在内存中,也可以保存在 redis 中,推荐使用后者),然后给这个 session 生成一个唯一的标识字符串 sessionId,然后在 响应头中种下这个唯一标识字符串。
  • 签名。这一步通过秘钥对 sessionId 进行签名处理,避免客户端修改 sessionId。(非必需步骤)
  • 浏览器中收到请求响应的时候会解析响应头,然后将 sessionId 保存在本地 cookie 中,浏览器在下次 http 请求的请求头中会自动带上该域名下的 cookie 信息。
  • 服务器在接受客户端请求时会去解析请求头 cookie 中的 sessionId,然后根据这个 sessionId 去找服务器端保存的该客户端的 session,然后判断该请求是否合法。

1.png

express-session

express-session 的常用参数:

  • secret:一个 String 类型的字符串,作为服务器端生成 session 的签名。

  • name:返回客户端的 key 的名称,默认为 connect.sid,也可以自己设置。

  • resave:(是否允许)当客户端并行发送多个请求时,其中一个请求在另一个请求结束时对 session 进行修改覆盖并保存。默认为 true。但是(后续版本)有可能默认失效,所以最好手动添加。

  • saveUninitialized:初始化 session 时是否保存到存储。默认为 true, 但是(后续版本)有可能默认失效,所以最好手动添加。

  • cookie:设置返回到前端 key 的属性,默认值为{ path: '/', httpOnly: true, secure: false, maxAge: null }。

express-session 的一些方法:

  • Session.destroy():删除 session,当检测到客户端关闭时调用。

  • Session.reload():当 session 有修改时,刷新 session。

  • Session.regenerate():将已有 session 初始化。

  • Session.save():保存 session。

Express 中使用 session-cookie 认证

  1. 安装 express-session 中间件 在 Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 Session 认证 npm i express-session
  2. 配置 express-session 中间件 express-session 中间件安装成功后,需要通过 app.use()来注册 session 中间件。
  3. 向 session 中存数据 当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息
  4. 清空 session 调用 req.session.destroy()函数,即可清空服务器保存的 session 信息。
const express = require("express")
const app = express()
const session = require("express-session")
app.use(express.json())
app.use(express.urlencoded())

app.use(express.static("./public"))

//配置session中间件
app.use(
    session({
        secret: "Hello World",
        resave: false,
        saveUninitialized: true,
        cookie: { maxAge: 1000 * 30 * 60 }
    })
)

app.use(function (req, res, next) {
    res.setHeader("Access-Control-Allow-Origin", "*")
    res.setHeader("Access-Control-Allow-Headers", "*")
    res.setHeader("Access-Control-Allow-Methods", "*")

    next()
})

app.post("/api/login", (req, res) => {
    const userinfo = req.body
    if (userinfo.username != "admin" || userinfo.password != "000000") {
        return res.send({
            status: 400,
            message: "登录失败"
        })
    }
    req.session.user = req.body //用户信息存储到session中
    req.session.islogin = true //存储登录状态

    console.log(req.session)
    res.send({
        status: 200,
        message: "登录成功"
    })
})

app.get("/admin/getinfo", (req, res) => {
    //可以直接从req.session对象上获取之前存储的数据
    if (!req.session.islogin) {
        return res.send({
            status: 400,
            message: "未登录"
        })
    }
    res.send({
        status: 200,
        message: "获取用户信息成功",
        username: req.session.user.username
    })
})

app.post("/api/logout", (req, res) => {
    req.session.destroy() //清空当前用户的session信息
    res.send({
        status: 200,
        message: "退出登录成功"
    })
})

app.listen(8080, function () {
    console.log("http://127.0.0.1:8080")
})

前端代码

login.onclick = function () {
    axios({
        url: "http://127.0.0.1:8080/api/login",
        method: "POST",
        data: {
            username: "admin",
            password: "000000"
        }
    }).then(res => {
        console.log(res.data.message)
    })
}

getInfo.onclick = function () {
    axios.get("http://127.0.0.1:8080/admin/getinfo").then(res => {
        if (res.data.status == 200) console.log(res.data.message)
        else console.log(res.data.message)
    })
}

logout.onclick = function () {
    axios.post("http://127.0.0.1:8080/api/logout").then(res => {
        console.log(res.data.message)
    })
}

Token

Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户名和密码。

JWT

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,这个 JSON 对象会被服务器端签名加密后返回给用户,返回的内容就是一张令牌,以后用户每次访问服务器端就带着这张令牌。

JWT 工作原理流程图

2.png

构成

Token,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以.进行拼接。其中头部和载荷都是以 JSON 格式存放数据,只是进行了 base64  编码(secret 部分是否进行 base64 编码是可选的,header 和 payload 则是必须进行 base64 编码),由于编码过程是可逆的,如果得知编码方式后,那么整个 jwt 串便是明文了,所以 pyaload 中一定不能放密码等重要信息。

header

头部主要是用来指明签名的算法,避免消息被篡改,jwt 中常用的签名算法是 HS256,常见的还有 md5,sha 等。

jwt 的头部承载两部分信息:

  1. typ(Type):令牌类型,也就是 JWT。
  2. alg(Algorithm) :签名算法,比如 HS256。 完整的头部就像下面这样的 JSON:
{
    "typ": "JWT",
    "alg": "HS256"
}

playload 负载主要是用来存放数据,一般可以存放相应用户数据来生成不同的 JWT

 "payload": {
        "data": [
          {
            "tooltt": "https://tooltt.com"
          }
        ],
        "iat": 1650451633,
        "exp": 1650556799
  }

这些有效信息包含三个部分:

  1. 标准中注册的声明 (建议但不强制使用) :
    • iss: jwt 签发者
    • sub: jwt 所面向的用户
    • aud: 接收 jwt 的一方
    • exp: jwt 的过期时间,这个过期时间必须要大于签发时间
    • nbf: JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
    • iat: jwt 的签发时间
    • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
  2. 公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
  3. 私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。

signature 签名是对头部和负载两个部分进行签名,防止数据篡改。签名里面有个核心就是要定义一个密钥,这个密钥只有服务器能知道,然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 公式如下:

Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey)

一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致

优点:

  • json 具有通用性,所以可以跨语言
  • 组成简单,字节占用小,便于传输
  • 服务端无需保存会话信息,很容易进行水平扩展
  • 一处生成,多处使用,可以在分布式系统中,解决单点登录问题
  • 可防护 CSRF 攻击

缺点:

  • jwt 模式的退出登录实际上是假的登录失效,因为只是浏览器端清除 token 形成的假象,假如用之前的 token 只要没过期仍然能够登陆成功
  • payload 部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息
  • 需要保护好加密密钥,一旦泄露后果不堪设想
  • 为避免 token 被劫持,最好使用 https 协议

Express 中使用 JWT

  1. 安装 JWT 相关的包
  • 运行命令
    npm install jsonwebtoken express-jwt
    
  • 包的作用
    • jsonwebtoken 用于生成 JWT 字符串
    • express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
  1. 导入 JWT 相关的包

  2. 定义 secret 秘钥 为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,需要专门定义一个用于加密和解密的 secret 秘钥

    • 当生成 JWT 字符串的时候,需要使用 secret 秘钥对用户信息进行加密, 最终得到加密好的 JWT 字符串
    • 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 秘钥进行解密
  3. 在登录成功后生成 JWT 字符串

    • 调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,响应给客户端
  4. 将 JWT 字符串还原为 JSON 对象 客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象

  5. 使用 req.auth 获取用户信息

    • 当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.auth 对象,来访问从 JWT 字符 串中解析出来的用户信息
    • 如果没有配置这个中间件, req 下是没有 auth 这个字段的
    • req.auth 可以获取哪些信息取决于我们加密了哪些信息
  6. 捕获解析 JWT 失败后产生的错误 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错 误,影响项目的正常运行。可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理

const express = require("express")
const app = express()

const jwt = require("jsonwebtoken")
const { expressjwt: expJWT } = require("express-jwt")

app.use(express.json())
app.use(express.urlencoded())

app.use(function (req, res, next) {
    res.setHeader("Access-Control-Allow-Origin", "*")
    res.setHeader("Access-Control-Allow-Headers", "*")
    res.setHeader("Access-Control-Allow-Methods", "*")
    next()
})

//定义secret密钥
const secretKey = "Hello World"

//用来解析token的中间件
app.use(
    expJWT({
        secret: secretKey,
        algorithms: ["HS256"] //加密格式
    }).unless({
        path: [/^\/api\//] //用来指定哪些接口不需要访问权限
    })
)
//这种密码学的方式使得token不需要存储,只要服务端能拿着密钥解析出用户信息,就说明该用户是合法的。
//若要更进一步的权限验证,需要判断解析出的用户身份是管理员还是普通用户。

app.post("/api/login", function (req, res) {
    //转存req.body请求体中的数据
    const userinfo = req.body

    //登录失败
    if (userinfo.username != "admin" || userinfo.password != "000000") {
        return res.send({
            status: 400,
            msg: "登录失败"
        })
    }

    //登录成功

    // 调用 jwt.sign() 生成JWT字符串, 三个参数分别是: 用户信息对象、加密秘钥、配置对象(token的有效期)
    //注意:千万不要将密码加密到token 字符串中
    const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: "360s" })
    res.send({
        status: 200,
        msg: "登录成功",
        token: "Bearer " + tokenStr
    }) //为了方便客户端使用Token,在服务器端直接拼接上Bearer 的前缀
})

//这是一个有权限的API接口
app.get("/admin/getinfo", function (req, res) {
    //使用req.auth.user获取用户信息
    res.send({
        status: 200,
        message: "获取用户信息成功",
        data: req.auth //要发送给客户端的用户信息
    })
})

//使用全局错误中间件,捕获解析JWT失败后产生的错误
app.use(function (err, req, res, next) {
    //token解析失败导致的错误
    if (err.name === "UnauthorizedError") {
        res.send({
            status: 401,
            message: "无效的token"
        })
    }

    //其它错误
    res.send({
        status: 500,
        message: "未知错误"
    })
})

app.listen(8080, function () {
    console.log("http://127.0.0.1:8080")
})

前端代码

login.onclick = function () {
    axios({
        url: "http://127.0.0.1:8080/api/login",
        method: "POST",
        data: {
            username: "admin",
            password: "000000"
        }
    }).then(res => {
        localStorage.setItem("token", res.data.token)
    })
} //登录方法:将后端返回的JWT存入localStorage

getInfo.onclick = function () {
    axios.get("http://127.0.0.1:8080/admin/getinfo").then(res => {
        if (res.data.status === 200) {
            console.log(res.data.data.username)
        } else {
            console.log(res.data.message)
        }
    })
}

logout.onclick = function () {
    localStorage.removeItem("token")
} //登出方法:删除JWT

//axios的请求拦截器,在每个request请求头上加JWT认证信息
axios.interceptors.request.use(
    config => {
        const token = window.localStorage.getItem("token")
        if (token) {
            // 判断是否存在token,如果存在的话,则每个http header都加上token
            // Bearer是JWT的认证头部信息
            config.headers["Authorization"] = token // "Bearer " + token;
        }
        return config
    },
    err => {
        return Promise.reject(err)
    }
)

jsonp

Jsonp(JSON with Padding)是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。

实现原理

  • 由于浏览器同源策略限制,网页无法通过 Ajax 请求非同源的接口数据。
  • script 标签不受浏览器同源策略的影响,可以通过 src 属性,请求非同源的 js 脚本数据。
  • 通过函数调用的形式,接收跨域接口响应回来的数据。
<script>
    //不能写type="module"
    function callbackFunction(obj) {
        console.log(obj)
    }

    get.onclick = function () {
        var oscript = document.createElement("script")
        oscript.src = "http://localhost:8080/?jsoncallback=callbackFunction"
        document.body.appendChild(oscript)
        oscript.onload = function () {
            //删除当前 节点
            oscript.remove()
        }
    }
</script>
const express = require("express")
const app = express()

app.get("/", (req, res) => {
    let fn = req.query.jsoncallback //callbackFunction
    let data = JSON.stringify({
        data: "Hello World"
    })
    res.send(fn + `(${data})`)
})

app.listen(8080, function () {
    console.log("http://127.0.0.1:8080")
})

缺点

  1. 只支持 GET 数据请求,不支持 POET 数据请求。
  2. 需要前后端协作。

闭包

闭包函数:声明在一个函数中的函数,叫做闭包函数。

闭包:内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。 特点

  • 让外部访问函数内部变量成为可能

  • 内部函数所用到的外部函数的变量,常驻内存,不会被销毁

  • 可以避免使用全局变量,防止全局变量污染

  • 函数内部返回一个函数,被外界所引用,这个内部函数就不会被销毁回收。会造成内存泄漏(有一块内存空间被长期占用,而不被释放)。解决:func = null

function outerFn() {
    var i = 0
    function innerFn() {
        i++
        console.log(i)
    }
    return innerFn
}
var inner = outerFn()
inner() //1
inner() //2
var inner2 = outerFn()
inner2() //1
inner2() //2
inner = null
inner2 = null

闭包应用

记住列表的索引

var oli = document.querySelectorAll("li")
// for (let i = 0; i < oli.length; i++) {
//     oli[i].onclick = function () {
//         console.log(i)
//     }
// }
for (var i = 0; i < oli.length; i++) {
    oli[i].onclick = (function (index) {
        return function () {
            console.log(11111, index)
        }
    })(i) //匿名自执行函数
}

函数柯里化

柯里化是把接收 n 个参数的 1 个函数改造为只接收 1 个参数的 n 个互相嵌套的函数的过程。也就是从 fn(a,b,c)变成 fn(a)(b)(c)。

作用:复用,减少不必要的固定参数

function FetchContainer(url) {
    return function (path) {
        return fetch(url + path)
    }
}
var fetcha = FetchContainer("http://www.a.com")
fetcha(" /aaa")
    .then(res => res.json())
    .then(res => console.log(res))
fetcha(" /bbb")
    .then(res => res.json())
    .then(res => console.log(res))
fetcha(" /ccc")
    .then(res => res.json())
    .then(res => console.log(res))
fetcha = null

var fetchb = FetchContainer("http://www.b.com")
fetchb(" /aaa")
    .then(res => res.json())
    .then(res => console.log(res))
fetchb(" /bbb")
    .then(res => res.json())
    .then(res => console.log(res))
fetchb(" /ccc")
    .then(res => res.json())
    .then(res => console.log(res))
fetchb = null

函数防抖

在浏览器的各种事件中,有一些容易频繁触发的事件,比如 scroll、resize、鼠标事件、键盘事件等。频繁触发回调导致大量的计算会引发页面抖动甚至卡顿,影响浏览器性能。防抖和节流就是控制事件触发的频率的两种手段。 防抖的中心思想是:在某段时间内,不管你触发了多少次回调,我都只执行最后一次。

var timer = null
mysearch.oninput = function () {
    if (timer) {
        clearTimeout(timer)
    }
    timer = setTimeout(function () {
        console.log("发ajax请求")
    }, 500)
}

闭包改进

mysearch.oninput = (function () {
    var timer = null
    return function () {
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(function () {
            console.log("发ajax请求")
        }, 500)
    }
})()

函数防抖的要点:

  • 需要一个 setTimeout 来延迟执行需要跑的代码。
  • 如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始。
  • 如果计时完毕,则执行代码。

函数节流

节流的中心思想是:在某段时间内,不管你触发了多少次回调,都只认第一次,并在计时结束时给予响应,也就是一段时间内只执行一次。

不一定要固定时间,执行完上次事件后才能执行下次事件也是节流

function throttle(interval) {
    let last = 0 //让第一次触发立即执行
    return function () {
        let now = new Date()
        if (now - last >= interval) {
            last = now
            console.log("触发了滚动事件")
        }
    } //也可以用定时器
}
document.addEventListener("scroll", throttle(1000))

Blob

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是 JavaScript 原生格式的数据。

Blob 构造函数 Blob(array[, options])

  • array 是一个由 ArrayBuffer, ArrayBufferView, Blob, string 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。string 会被编码为 UTF-8。
  • options 是一个可选的对象,它可能会指定如下两个属性:
    • type,默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。
    • endings,默认值为"transparent",用于指定包含行结束符\n 的字符串如何被写入。 它是以下两个值中的一个: "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持 blob 中保存的结束符不变。

示例:

var content1 = ["This is my firt trip to an island"]
var blob1 = new Blob(content, { type: "text/plain" })
var content2 = { name: "Alice", age: 23 }
var blob2 = new Blob([JSON.stringify(content2, null, 2)], { type: "application/json" })

Blob 实例属性

属性名称 读/写 描述
size 只读 Blob 对象中所包含数据的大小(字节)。
type 只读 一个字符串,表明该Blob对象所包含数据的MIME类型。如果类型未知,则该值为空字符串。例如 "image/png"。

示例:

var content = ['<div id="box"><p class="pra">a paragraph</p></div>']
var blob = new Blob(content, { type: "text/html" })
console.log(blob.size) // 50
console.log(blob.type) // text/html

Blob 实例方法

  • slice([start[, end[, contentType]]])

slice 方法接收三个可选参数,startend 都是数值,表示截取的范围,contentType 指定截取的内容的 MIME 类型。返回一个新的 Blob对象。

var blob = new Blob(["This is an example of Blob slice method"], { type: "text/plain" })
console.log(blob.size) // 39
var newBlob = blob.slice(10, 20, "text/plain")
console.log(newBlob.size) // 10

Blob 对象中读取内容可以使用 FileReader.

File

File 构造函数

我们接触的多数关于 File 的操作都是读取,js 也为我们提供了手动创建 File 对象的构造函数:File(bits, name[, options])

  • bits (required) ArrayBuffer,ArrayBufferView,Blob,或者 Array[string] — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容。

  • name [String] (required) 文件名称,或者文件路径.

  • options [Object] 选项对象,包含文件的可选属性。可用的选项如下:

    • type: string, 表示将要放到文件中的内容的 MIME 类型。默认值为 '' 。

    • lastModified: 数值,表示文件最后修改时间的 Unix 时间戳(毫秒)。默认值为当前时间毫秒值。

示例:

var file1 = new File(["text1", "text2"], "test.txt", { type: "text/plain" })

根据已有的 blob 对象创建 File 对象:

var file2 = new File([blob], "test.png", { type: "image/png" })

File 实例属性

File 对象的实例内容不可见,但是有以下属性可以访问:

属性名称 读/写 描述
name 只读 返回文件的名称.由于安全原因,返回的值并不包含文件路径 。
type 只读 返回 File 对象所表示文件的媒体类型(MIME)。例如 PNG 图像是 "image/png".
lastModified 只读 number, 返回所引用文件最后修改日期,自 1970年1月1日0:00 以来的毫秒数。
lastModifiedDate 只读 Date, 返回当前文件的最后修改日期,如果无法获取到文件的最后修改日期,则使用当前日期来替代。

示例:

<input type="file" id="file" />
document.getElementById("file").addEventListener("change", function (event) {
    const file = this.files[0]
    if (file) {
        console.log(file.name)
        console.log(file.size)
        console.log(file.lastModified)
        console.log(file.lastModifiedDate)
    }
})

备注: 基于当前的实现,浏览器不会实际读取文件的字节流,来判断它的媒体类型。它基于文件扩展来假设,将 PNG 图像文件的后缀名重命名为 .txt,那么读取的该文件的 type 属性值为 "text/plain", 而不是 "image/png" 。而且,file.type 仅仅对常见文件类型可靠。例如图像、文档、音频和视频。不常见的文件扩展名会返回空字符串。开发者最好不要依靠这个属性,作为唯一的验证方案。

File 实例方法

  • slice([start[, end[, contentType]]])

File 对象没有定义额外的方法,由于继承了 Blob 对象,也就继承了 slice 方法,用法同上文 Blob 的 slice 方法。

FileReader, URL.createObjectURL(), createImageBitmap(), 及 XMLHttpRequest.send() 都能处理 Blob 和 File。

FileReader

FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。

其中 File 对象可以是来自用户在一个 <input> 元素上选择文件后返回的 FileList, 也可以来自拖放操作生成的 DataTransfer 对象,还可以是来自在一个 HTMLCanvasElement 上执行 mozGetAsFile() 方法后返回结果。

FileReader 构造函数

var reader = new FileReader()

构造函数不需要传入参数,返回一个 FileReader 的实例。FileReader 继承 EventTarget对象。

FileReader 实例属性

属性名称 读/写 描述
error 只读 DOMException 的实例,表示在读取文件时发生的错误 。
result 只读 文件的内容,该属性仅在读取操作完成后(load)后才有效,格式取决于读取方法
readyState 只读 表示读取文件时状态的数字

备注: readeyState的取值如下:

常量名 描述
0 EMPTY 还没有加载任何数据
1 LOADING 数据正在被加载
2 DONE 已完成全部的读取请求

使用示例:

var reader = new FileReader()
console.log(reader.error) // null
console.log(reader.result) // null
console.log(reader.readyState) // 0
console.log(reader.EMPTY) // 0
console.log(reader.LOADING) // 1
console.log(reader.DONE) // 2

EMPTY、LOADING、DONE 这三个属性同时存在于 FileReader 和它的的原型对象上,因此实例上有这三个属性,FileReader 对象本身也有这三个属性:

console.log(FileReader.EMPTY) // 0
console.log(FileReader.LOADING) // 1
console.log(FileReader.DONE) // 2

FileReader 事件

文件的读取是一个异步的过程,和 XMLHttpRequest 对象一样,在读取操作过程中会触发一系列事件。

事件名称 描述 使用示例
abort 当读取操作被中止时调用 reader.onabort = function(event) {}
error 当读取操作发生错误时调用 reader.onerror = function(event) {}
load 当读取操作成功完成时调用 reader.addEventListener('load', function(event) {})
loadstart 当读取操作将要开始之前调用 reader.onloadstart = function(event) {}
loadend 当读取操作完成时调用,不管是成功还是失败 reader.onloadend = function(event) {}
progress 在读取数据过程中周期性调用 reader.onprogress = function(event) {}

FileReader 实例方法

FileReader 的实例具有以下可操作的方法:

方法名称 描述 使用示例
abort() 手动终止读取操作,只有当readyState 为 1 时才能调用,调用后,readyState 值为 2 reader.abort()
readAsArrayBuffer(blob) 读取指定的 Blob 或 File 对象。读取操作完成后(触发loadend事件),result属性将包含一个 ArrayBuffer 对象表示所读取的文件的数据。 reader.readAsArrayBuffer(blob)
readAsDataURL(blob) 读取指定的 Blob 或 File 对象。读取操作完成后(触发loadend事件),result属性将包含一个 data:URL 格式的字符串(base64编码) reader.readAsDataURL(blob)
readAsBinaryString(blob) 已废弃,用 readAsArrayBuffer 代替 --
readAsText(blob[,encoding]) 将 Blob 或者 File 对象转根据特殊的编码格式转化为内容(字符串形式), 默认编码是 utf-8 reader.readAsText(blob)

读取本地图片示例:

<input type="file" id="file" accept="image/png, image/jpg, image/jpeg, image/gif" />

<img src="" alt="Image preview..." />
var preview = document.querySelector("img")
var reader = new FileReader()

reader.addEventListener(
    "load",
    function () {
        preview.src = reader.result
    },
    false
)

document.getElementById("file").addEventListener("change", function (event) {
    var file = this.files[0]
    if (file) {
        reader.readAsDataURL(file)
    }
})

dataURLbase64编码的数据格式,展示类型为字符串,形如: data:image/jpeg;base64,/9j/4QXERXhpZgAATU...dataURL 转为 blob对象:

function dataURLToBlob(dataurl) {
    let arr = dataurl.split(",")
    let mime = arr[0].match(/:(.*?);/)[1]
    let bstr = atob(arr[1])
    let n = bstr.length
    let u8arr = new Uint8Array(n)
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n)
    }
    return new Blob([u8arr], { type: mime })
}

结合上例,根据已有的 <img> 对象创建一个 File 对象:

reader.addEventListener(
    "load",
    function () {
        preview.src = reader.result
        var blob = dataURLToBlob(reader.result)
        var newFile = new File([blob], "test.jpeg", { type: blob.type })
        console.log(newFile.name) // test.jpeg
        console.log(newFile.type)
        console.log(newFile.size)
    },
    false
)

URL.createObjectURL 将图片文件转换成 data:URL 格式供 <img> 元素展示,除了使用 fileReader.readAsDataURL 外,还可以使用 URL.createObjectURL 方法。 URL.createObjectURL(blob) 方法返回一个 blob: 开头的字符串,指向文件在内存中的地址。

<input type="file" id="file" accept="image/png, image/jpg, image/jpeg, image/gif" />
<br />
<img src="" alt="Image preview..." />
var preview = document.querySelector("img")
document.getElementById("file").addEventListener("change", function (event) {
    var file = this.files[0]
    if (file) {
        preview.src = URL.createObjectURL(file)
    }
})