this
this 是一个指针型变量,它动态指向当前函数的运行环境。
-
在全局作用域下,this 始终指向全局对象 window,无论是否是严格模式
console.log(this) -
函数内的 this
-
严格模式下:
//严格模式下 function test() { "use strict" console.log(this) } test() //undefined window.test() //window直接 test()调用函数,this 指向 undefined,window.test()调用函数 this 指向 window。
-
非严格模式下:
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() //小红 -
隐式丢失
-
被隐式绑定的函数丢失了绑定对象,从而默认绑定到 window
function foo1() { console.log(this) // window } var obj6 = { a: 2, foo: foo1 } var bar = obj6.foo // 在这并未执行方法 bar() // 在这执行了方法 -
参数传递
function foo3() { console.log(this) } function bar1(fn) { // 默认赋值 fn = obj7.foo fn() // window } var obj7 = { a: 1, foo: foo3 } bar1(obj7.foo) -
setTimeout() 和 setInterval() 第一个参数的回调函数中的 this 默认指向 window
setTimeout(function () { console.log(this) // window }, 0)
-
-
箭头函数的 this 箭头函数中的 this 指向外层函数(非箭头函数)的作用域中的 this 指向。 箭头函数的 this 指向在被定义的时候就确定了,之后永远都不会改变。即使使用 call()、apply()、bind()等方法改变 this 指向也不可以。
所有绑定规则不适应箭头函数。
-
默认绑定规则(独立调用对箭头函数)无效
function foo() { console.log(this) // obj // 箭头函数 var test = () => { console.log(this) // obj } return test } var obj = { a: 1, foo: foo } obj.foo()() -
显示绑定无效
function foo() { console.log(this) // window var test = () => { console.log(this) // window } return test } var obj2 = { a: 2 } foo().call(obj2) -
隐式绑定无效
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 有三种类型的属性
- 命名数据属性:拥有一个确定的值的属性。这也是最常见的属性
- 命名访问器属性:通过 getter 和 setter 进行读取和赋值的属性
- 内部属性:由 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" }
注意点:
-
浅拷贝 Object.assign 方法实行的是浅拷贝
-
数组的处理 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'} -
取值函数的处理 Object.assign 只能进行值的复制,如果要复制的值是一个取值函数, 那么将求值后再复制。
const sources = { get foo() { return 1 } } const target1 = {} console.log(Object.assign(target1, sources)) //{foo: 1}
面向对象
- 面向对象不是语法,是一个思想,是一种编程模式
- 面向过程:关注着过程的编程模式
- 面向对象:关注着对象的编程模式
面向对象基本特征
- 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
- 多态:对象的多功能,多方法,一个方法多种表现形式。
对象实例化方式
工厂模式
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(原型) 属性,这个属性是一个指针,默认指向一个空对象,可以用来存储让所有实例共享的属性和方法。
-
每一个构造函数都拥有一个 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 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链。
所有原型链的终点都是 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]
当原型上面的属性是一个引用类型的值时,我们通过其中某一个实例对原型属性的更改,结果会反映在所有实例上面。
对象和函数的关系
对象是由函数构造出来的。
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
继承
继承的本质就是复制,即重写原型对象,代之以一个新类型的实例。
-
原型链继承
原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。
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" -
借用构造函数继承 使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
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 中的属性复制一份。
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
-
组合继承 组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。
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 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
-
原型式继承 利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
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 方法。
-
寄生式继承 核心:在原型式继承的基础上,增强对象,返回构造函数
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"缺点(同原型式继承): 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。 无法传递参数
-
寄生组合式继承 结合借用构造函数传递参数和寄生模式实现继承
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() 这是最成熟的方法,也是现在库实现的方法
-
混入方式继承多个对象
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 的方法。
-
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) // 输出 100extends 继承的核心代码如下,其实现和上述的寄生组合式继承方式一样
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 给属性赋值时,是给子类的属性赋值,访问时是指向父类属性
静态方法/函数
- 类的静态方法
function BaseClass() {}
// 类添加add函数
BaseClass.add = function () {
console.log("BaseClass add()方法被调用")
}
BaseClass.add() //BaseClass add()方法被调用
var instance = new BaseClass()
// 实例不能调用类方法(即类的静态方法)
//instance.add();
- 类的静态属性
function BaseClass(params) {}
// 类添加静态变量 nameTest
BaseClass.nameTest = "jadeshu"
console.log(BaseClass.nameTest) // jadeshu
var instance = new BaseClass()
// 实例不能调用类的静态成员变量)
console.log(instance.nameTest) // undefined
- 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 其实就是浏览器与服务器之间的一种异步通信方式
- 不需要插件的支持,原生 js 就可以使用
- 用户体验好(不需要刷新页面就可以更新数据)
- 减轻服务端和带宽的负担
- 缺点:搜索引擎的支持度不够,因为数据都不在页面上,搜索引擎搜索不到
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
-
抽象表达:
- Promise 是一门新的技术(ES6 规范)
- Promise 是 JS 中进行异步编程的新解决方案
备注:旧方案是单纯使用回调函数
-
具体表达:
- 从语法上来说:Promise 是一个构造函数
- 从功能上来说:promise 对象用来封装一个异步操作并可以获取其成功/ 失败的结果值
异步编程
//fs文件操作
require("fs").readFile("./index.html", (err, data) => {})
//数据库操作
//AJAX
$.get("/server", data => {})
//定时器
setTimeout(() => {}, 2000)
优点:
- 指定回调函数的方式更加灵活
- 旧的:必须在启动异步任务前指定
- promise: 启动异步任务=>返回 promise 对象=>给 promise 对象绑定回调函 数(甚至可以在异步任务结束后指定/多个)
- 支持链式调用,可以解决回调地狱问题
- 什么是回调地狱 回调函数嵌套调用,外回调函数异步执行的结果是嵌套的回调执行的条件
- 回调地狱的缺点
- 不便于阅读
- 不便于异常处理
- 解决方案 promise 链式调用
Promise 的状态 实例对象中的一个属性「PromiseState」
- pending:未决定的
- resolved / fullfilled:成功
- rejected:失败
状态改变
- pending 变为 resolved
- pending 变为 rejected
说明:只有这 2 种,且一个 promise 对象只能改变一次,无论变为成功还是失败,都会有一个结果数据
Promise 对象的值 实例对象中的另一个属性「PromiseResult」,保存着异步任务「成功/失败」的结果(resolve/reject)
基本流程
使用
-
Promise 构造函数: Promise(excutor){}
- executor 函数:执行器(resolve, reject)=> {}
- resolve 函数:内部定义成功时我们调用的函数 value=> {}
- reject 函数:内部定义失败时我们调用的函数 reason=> {}
说明:executor 会在 Promise 内部立即同步调用,异步操作在执行器中执行
-
Promise.prototype.then 方法: (onResolved, onRejected)=> {}
- onResolved 函数:成功的回调函数(value) => {}
- onRejected 函数:失败的回调函数(reason) => {}
说明:指定用于得到成功 result 的成功回调和用于得到失败 error 的失败回调 返回一个新的 promise 对象
-
Promise.prototype.catch 方法: (onRejected) => {}
- onRejected 函数:失败的回调函数(reason)=> {}
-
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 }) -
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 }) }) -
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])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]) -
Promise.race(promises) 方法:
- promises: 包含 n 个 promise 的数组
说明:异步执行,返回一个新的 promise,第一个完成的 promise 的结果、状态就是最终的结果、状态
关键问题
-
如何改变 promise 的状态?
- resolve(value): 如果当前是 pending 就会变为 resolved
- reject(reason): 如果当前是 pending 就会变为 rejected
- 抛出异常:如果当前是 pending 就会变为 rejected
-
一个 promise 指定多个成功/失败回调函数,都会调用吗? 当 promise 改变为对应状态时都会调用
let p = new Promise((resolve, reject) => { resolve("OK") }) p.then(value => { console.log(value) }) p.then(value => { alert(value) }) -
改变 promise 状态和指定回调函数谁先谁后?
- 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调
//指定回调,改变状态,执行对应回调 let p = new Promise((resolve, reject) => { setTimeout(() => { resolve("OK") }, 1000) }) p.then( value => { console.log(value) }, reason => {} )- 如何先改状态再指定回调?
- 在执行器中直接调用 resolve()/reject()
- 延迟更长时间才调用 then()
- 什么时候才能得到数据?
- 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据
- 如果先改变的状态, 那当指定回调时,回调函数就会调用,得到数据
-
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 -
串连多个操作任务
- promise 的 then()返回一个新的 promise,可以形成 then()的链式调用
- 通过 then 的链式调用串连多个同步/异步任务
-
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 -
-
中断 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 函数
- 函数的返回值为 promise 对象
- 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 表达式
- await 右侧的表达式一般为 promise 对象,但也可以是其它的值
- 如果表达式是 promise 对象,await 返回的是 promise 成功的值
- 如果表达式是其它值,直接将此值作为 await 的返回值
注意
- await 必须写在 async 函数中,但 async 函数中可以没有 await
- 如果 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,然后判断该请求是否合法。
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 认证
- 安装 express-session 中间件
在 Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 Session 认证
npm i express-session - 配置 express-session 中间件 express-session 中间件安装成功后,需要通过 app.use()来注册 session 中间件。
- 向 session 中存数据 当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息
- 清空 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 工作原理流程图
构成
Token,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以.进行拼接。其中头部和载荷都是以 JSON 格式存放数据,只是进行了 base64 编码(secret 部分是否进行 base64 编码是可选的,header 和 payload 则是必须进行 base64 编码),由于编码过程是可逆的,如果得知编码方式后,那么整个 jwt 串便是明文了,所以 pyaload 中一定不能放密码等重要信息。
header
头部主要是用来指明签名的算法,避免消息被篡改,jwt 中常用的签名算法是 HS256,常见的还有 md5,sha 等。
jwt 的头部承载两部分信息:
- typ(Type):令牌类型,也就是 JWT。
- alg(Algorithm) :签名算法,比如 HS256。 完整的头部就像下面这样的 JSON:
{
"typ": "JWT",
"alg": "HS256"
}
playload 负载主要是用来存放数据,一般可以存放相应用户数据来生成不同的 JWT
"payload": {
"data": [
{
"tooltt": "https://tooltt.com"
}
],
"iat": 1650451633,
"exp": 1650556799
}
这些有效信息包含三个部分:
- 标准中注册的声明 (建议但不强制使用) :
- iss: jwt 签发者
- sub: jwt 所面向的用户
- aud: 接收 jwt 的一方
- exp: jwt 的过期时间,这个过期时间必须要大于签发时间
- nbf: JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
- iat: jwt 的签发时间
- jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
- 公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
- 私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。
signature 签名是对头部和负载两个部分进行签名,防止数据篡改。签名里面有个核心就是要定义一个密钥,这个密钥只有服务器能知道,然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 公式如下:
Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey)
一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致
优点:
- json 具有通用性,所以可以跨语言
- 组成简单,字节占用小,便于传输
- 服务端无需保存会话信息,很容易进行水平扩展
- 一处生成,多处使用,可以在分布式系统中,解决单点登录问题
- 可防护 CSRF 攻击
缺点:
- jwt 模式的退出登录实际上是假的登录失效,因为只是浏览器端清除 token 形成的假象,假如用之前的 token 只要没过期仍然能够登陆成功
- payload 部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息
- 需要保护好加密密钥,一旦泄露后果不堪设想
- 为避免 token 被劫持,最好使用 https 协议
Express 中使用 JWT
- 安装 JWT 相关的包
- 运行命令
npm install jsonwebtoken express-jwt - 包的作用
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
-
导入 JWT 相关的包
-
定义 secret 秘钥 为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,需要专门定义一个用于加密和解密的 secret 秘钥
- 当生成 JWT 字符串的时候,需要使用 secret 秘钥对用户信息进行加密, 最终得到加密好的 JWT 字符串
- 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 秘钥进行解密
-
在登录成功后生成 JWT 字符串
- 调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,响应给客户端
-
将 JWT 字符串还原为 JSON 对象 客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象
-
使用 req.auth 获取用户信息
- 当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.auth 对象,来访问从 JWT 字符 串中解析出来的用户信息
- 如果没有配置这个中间件, req 下是没有 auth 这个字段的
- req.auth 可以获取哪些信息取决于我们加密了哪些信息
-
捕获解析 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")
})
缺点
- 只支持 GET 数据请求,不支持 POET 数据请求。
- 需要前后端协作。
闭包
闭包函数:声明在一个函数中的函数,叫做闭包函数。
闭包:内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。 特点
-
让外部访问函数内部变量成为可能
-
内部函数所用到的外部函数的变量,常驻内存,不会被销毁
-
可以避免使用全局变量,防止全局变量污染
-
函数内部返回一个函数,被外界所引用,这个内部函数就不会被销毁回收。会造成内存泄漏(有一块内存空间被长期占用,而不被释放)。解决: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 方法接收三个可选参数,start 和 end 都是数值,表示截取的范围,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 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用File或Blob对象指定要读取的文件或数据。
其中 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)
}
})
dataURL是base64编码的数据格式,展示类型为字符串,形如: 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)
}
})