文末
js前端的重头戏,值得花大部分时间学习。
推荐通过书籍学习,《 JavaScript 高级程序设计(第 4 版)》你值得拥有。整本书内容质量都很高,尤其是前十章语言基础部分,建议多读几遍。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
另外,大推一个网上教程 现代 JavaScript 教程 ,文章深入浅出,很容易理解,上面的内容几乎都是重点,而且充分发挥了网上教程的时效性和资料链接。
学习资料在精不在多,二者结合,定能构建你的 JavaScript 知识体系。
面试本质也是考试,面试题就起到很好的考纲作用。想要取得优秀的面试成绩,刷面试题是必须的,除非你样样精通。
这是288页的前端面试题
}
};
var a = 3;
obj2.foo1(); // 2
obj2.foo2(); // setTimeout 是浏览器自带对象方法,所以this指向了window对象
// Window {window: Window, self: Window, document: document, name: ‘tzw’, location: Location, …}
// 3
// ===============================================================================================================
// eg2:
debugger ;var obj1 = {
a: 1
};
var obj2 = {
a: 2,
foo1: function() {
console.log(this.a)
},
foo2: function() {
setTimeout(function() {
console.log(this);
console.log(this.a)
}.call(obj1), 0)
}
};
var a = 3;
obj2.foo1(); // 2
obj2.foo2();
// {a:1}
// 1
// ===============================================================================================================
// eg3:
debugger ;var obj1 = {
a: 1
};
var obj2 = {
a: 2,
foo1: function() {
console.log(this.a)
},
foo2: function() {
function inner() {
console.log(this);
console.log(this.a)
}
inner()
}
};
var a = 3;
obj2.foo1(); // 2
obj2.foo2();
// Window {window: Window, self: Window, document: document, name: ‘tzw’, location: Location, …}
// 3
// ===============================================================================================================
// eg4:
debugger ;var obj1 = {
a: 1
};
var obj2 = {
a: 2,
foo1: function() {
console.log(this.a)
},
foo2: function() {
function inner() {
console.log(this);
console.log(this.a)
}
inner.call(obj1)
}
};
var a = 3;
obj2.foo1(); // 2
obj2.foo2()
// {a: 1}
// 1
// ===============================================================================================================
// eg5:
debugger ;function foo() {
console.log(this.a);
return function() {
console.log(this.a)
}
}
;var obj = {
a: 1
};
var a = 2;
foo(); // 2
foo.bind(obj); // 不会打印在控制台上,中间被吞了的原因是因为控制台只会返回最后一个
foo().bind(obj)
// 2
// (){console.log(this.a)}
#### 4) new绑定
* 当使用 new 关键字调用函数时,函数中的 this 一定是js创建的新对象
* 使用new调用函数时,会执行如下步骤:
+ 创建(或者构造)一个全新的对象
+ 这个新对象会被执行`[[prototype]]`连接
+ 这个新对象会绑定到函数调用的this
+ 如果函数没有返回其它对象,那么表达式中的函数调用会自动返回这个新对象。一句话概括就是:当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造函数里的this就指向返回的这个对象。
debugger; let Myclass = function() {
this.name = ‘tzw’;
};
let obj = new Myclass(); // 构造函数里的this就指向返回的这个对象
obj.name; // ‘tzw’
// *如果构造函数显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前的this。
let Info = function(){
this.name = ‘qdd’;
return { //显式地返回一个对象
name: ‘tzw’
}
};
let obj = new Info();
obj.name; // ‘tzw’
// *如果构造函数不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题,主要是new关键字调用函数,函数内部隐式返回this造成的。
// 典例
// eg1:
debugger ;function Person(name) {
this.name = name;
this.foo1 = function() {
console.log(this.name)
}
;
this.foo2 = function() {
return function() {
console.log(this.name)
}
}
}
;var person1 = new Person(‘person1’);
person1.foo1(); // 1
person1.foo2()(); // ‘’
// *wind.name 不会被回收,刷新页面依然还在,有些网站会设置这个属性并检测
// ===============================================================================================================
// eg2:
debugger ;var name = ‘window’;
function Person(name) {
this.name = name;
this.foo = function() {
console.log(this.name);
return function() {
console.log(this.name)
}
}
}
;var person1 = new Person(‘person1’);
var person2 = new Person(‘person2’);
person1.foo.call(person2)();
// person2
// window
person1.foo().call(person2);
// person1
// person2
#### 5) 箭头函数绑定
* 创建箭头函数时,就已经确定了它的 this 指向。
* 箭头函数没有自己的this指向,它会捕获自己定义所处的外层执行环境,并且继承这个this值,指向当前定义时所在的对象。箭头函数的this指向在被定义的时候就确定了,之后永远都不会改变。即使使用 call()、 apply() 、 bind()等方法改变this指向也不可以。
* 箭头函数的重要特征:箭头函数中没有this和arguments。
debugger ;var obj = {
name: ‘obj’,
foo1: ()=>{
console.log(this.name)
}
,
foo2: function() {
console.log(this.name);
return ()=>{ // 指向外层作用域
console.log(this.name)
}
}
};
var name = ‘window’;
obj.foo1();
// window foo1的对象是obj, obj是全局下定义的对象
obj.foo2()();
// obj
// obj // 因为返回的是箭头函数,而它又是foo2属性的匿名函数的返回值,所以指向了foo2属性当前作用域下的name属性
obj2 = {
name: ‘obj2’
};
obj.foo1.call(obj2);
// window 因为call对箭头函数没有影响
//【总结:】箭头函数内的this是由外层作用域决定的
// 补充,如果箭头函数被赋给了一个变量
// function Fun() {
this.name = () => { console.log(this); } // 等同于 this.name = function() {console.log(this);}
}
fun = new Fun();
fun.name() // Fun {name: ƒ} 这个this指向构造函数本身
**原型链中的this**: this这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它 所属于的对象。
#### 总结
>
> **1.函数外面的this,即全局作用域的this指向window**
>
>
> \*\*2.函数里面的this总是指向直接调用者;如果没有直接调用者,隐含的调用者是window \*\*
>
>
> \*\*3.用new调用一个函数,这个函数即为构造函数。构造函数里面的this是和实例对象沟通 的桥梁,它指向实例对象 \*\*
>
>
> \*\*4.事件回调里面,this指向绑定事件的对象,而不是触发事件的对象。当然这两个可以是一样的 \*\*
>
>
> **5.箭头函数内的this由外层作用域决定**
>
>
> 简单来说 this 的指向跟函数的调用位置紧密相关,要想知道函数调用时 this 到底引用了什么,就应该明确函数的调用位置。
>
>
>
### 3. 面向对象
#### 1) 封装
* 封装就是将变量和方法包装在一个单元中,其唯一目的是从外部类中隐藏数据。这使得程序结构更易于管理,因为每个对象的实现和状态都隐藏在明确定义的边界之后。
* 封装,在ES6之前的使用的是构造函数,ES6之后用的是class【写前端常用但是逆向少见】
>
> ES6的class实际就是一个语法糖,在ES6之前,是没有类这个概念的,因此是借助于原型对象和构造函数来实现。
>
>
> 1. 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var等声明的属性)
> 2. 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
> 3. 静态属性和方法:定义在构造函数上的方法(比如Foo.xxx),不需要实例就可以调用
>
>
>
// 1.静态属性方法和公有属性方法
debugger ;function Foo(arg1, arg2) {
var private1 = ‘pri1’; // *在函数内用var等定义的就是私有的
var private2 = ‘pri2’;
var private3 = function() {
console.log(private1 + private2)
};
this.pub1 = arg1; //*在函数内用this承接的就是公有的
this.pub2 = arg2;
this.pub3 = function() {
private3();
console.log(‘finish’)
}
}
Foo.descript = ‘1这是一段讲述静态属性方法的代码1’;
Foo.descript2 = function() {
console.log(‘2这是一段讲述静态属性方法的代码2’)
}
;
Foo.prototype.descript3 = function() {
console.log(‘1这是一段讲述公有属性方法的代码1’)
}
;
var foo = new Foo(‘arg1’,‘arg2’);
console.log(Foo.descript); // 1这是一段讲述静态属性方法的代码1
Foo.descript2(); // 2这是一段讲述静态属性方法的代码2
console.log(foo.descript); // undefined
foo.descript3(); // 1这是一段讲述静态属性方法的代码1
// 在构造函数上也就是使用Foo.xxx定义的是静态属性和方法,静态属性指的是Class本身的属性,而不是定义在实例对象(this)上的属性。
// 在构造函数内使用this设置,或者设置在构造函数原型对象上比如Foo.prototype.xxx,就是公有属性和方法(实例方法)
// ===============================================================================================================
// 2.定义在构造函数原型对象上的属性和方法不能直接表现在实例对象上,但是实例对象可以访问或者调用它们
// ===============================================================================================================
// 3. 在ES6之后,新增了class 这个关键字。它可以用来代替构造函数,达到创建“一类实例”的效果;
// 并且类的数据类型就是函数,所以用法上和构造函数很像,直接用new命令来配合它创建一个实例:
// 类的所有方法都定义在类的prototype属性上面。
debugger ;class Foo {
constructor() {
// constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法;
// 一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加;
// constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
var p1 = ‘luck’;
this.p2 = ‘foo’;
this.p3 = function() {}
};
p4 = ‘white’;
p5 = function() {
console.log(‘我追你如果我追到你’)
};
p6() { // *这个方法定义当前类的原型上
console.log(‘我就把你嘿嘿嘿’)
}
}
var foo = new Foo();
console.log(foo); // { p4: ‘white’, p2: ‘foo’, p3: f, p5: f}
foo.p5(); // ‘我追你如果我追到你’
foo.p6(); // ‘我就把你嘿嘿嘿’
// ===============================================================================================================
// 4.可以使用static标识符表示它是一个静态的属性或者方法,
// 加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
// ===============================================================================================================
// 5.class的变量不会提升
// ===============================================================================================================
// 6.作用域
debugger ;class Cat {
constructor() {
this.name = ‘guaiguai’;
var type = ‘constructor’
}
type = ‘class’;
// *这里的type是新写法,等同于this.type, 新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面;
// 不带this的写法的优先级比带this的优先级高
getType = function() {
console.log(this.type);
console.log(type) // 这里的type只的是全局下的
}
}
var type = ‘window’;
var guaiguai = new Cat();
guaiguai.getType();
// ‘class’
// ‘window’ 去全局里边的取得type, class的type属性是cat的原型对象里边的
// ===============================================================================================================
// 7.闭包加深
debugger ;class Cat {
constructor() {
this.name = ‘guaiguai’;
var type = ‘constructor’;
this.getType = ()=>{
console.log(this.type);
console.log(type)
}
}
type = ‘class’;
getType = ()=>{
console.log(this.type);
console.log(type)
}
}
var type = ‘window’;
var guaiguai = new Cat();
guaiguai.getType(); // constructor this.getType()取得是闭包里边携带的type变量
console.log(guaiguai); // Cat {type: ‘class’, name: ‘guaiguai’, getType: ƒ}
#### 2) 继承
* 继承是指从多种实现类中抽象出一个基类,使其具备多种实现类的共同特性。比如从猫类、狗类、虎类中可以抽象出一个动物类,具有猫、狗、虎类的共同特性(吃、跑、叫等)。
##### (1) 原型链继承
function Parent () {
this.name = ‘Parent’
this.sex = ‘boy’
}
Parent.prototype.getName = function () {
console.log(this.name)
console.log(this.sex)
}
function Child () {
this.name = ‘child’
}
Child.prototype = new Parent() // 将Child.prototype 指向Parent的实例
child1 = new Child() // 创建了一个Child对象child1
// Child {name: ‘child’}
child1.proto.proto == Parent.prototype
// true child1对象的原型(proto)指向了Child.prototype, 而Child.prototype的原型(proto)指向了Parent.prototype
child1.getName()
// child // 这个this.name 是从child1对象获取的
// boy // child1对象没有sex属性,
console.log(child1)
// Child {name: ‘child’} [[Prototype]]: { Parent name: “Parent” sex: “boy” } [[Prototype]]: Object
child1.sex = ‘girl’ // 直接修改对象上的属性,是给本对象上添加一个新属性,不会修改原型引用上的sex
console.log(Child.prototype.proto == Parent.prototype)
// true // 这种方式就叫做原型链继承,将子类的原型对象指向父类的实例
// ===============================================================================================================
// 缺陷
function A () {}
function B () {
this.name = ‘anlan’
}
function C () {}
B.prototype = new C()
A.prototype = new B()
a = new A()
A.prototype.proto == B.prototype // true
a.proto // C {name: ‘anlan’} // 这是一个显示问题,正常来讲a.proto 指向的应该是B的原型
// ===============================================================================================================
function Parent (name) {
this.name = name
this.sex = ‘boy’
this.colors = [‘white’, ‘black’] // 引用类型,取得是内存地址
}
function Child (name) {
this.name = name
this.feature = [‘cute’]
}
var parent = new Parent(‘parent’)
Child.prototype = parent
var child1 = new Child(‘child1’)
child1.sex = ‘girl’ // 是给本对象上添加一个新属性,不会修改原型引用上的sex
child1 // {“name”: “child1”, “feature”: [“cute”],“sex”: “girl”}
child1.colors.push(‘yellow’) // Parent原型的colors属性新增了一个yellow
parent // {“name”: “parent”, “sex”: “boy”, “colors”: [“white”, “black”, “yellow”]}
child1.feature.push(‘sunshine’) // child1对象本身有feature,所以是在本对象的属性上添加了一个sunshine
child1 // {“name”: “child1”, “feature”: [“cute”, “sunshine”],“sex”: “girl”}
var child2 = new Child(‘child2’)
child2 // Child {name: ‘child2’, feature: Array(1)}feature : [‘cute’]name : “child2” [[Prototype]]:Parent colors : (3) [‘white’, ‘black’, ‘yellow’]name : "parent"sex : “boy” [[Prototype]]:Object 可以看出原型对象的所有属性都被共享了
// isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链中。
// isPrototypeOf 实际上是instanceof 的反向。它是用来判断指定对象object1是否存在于另一个对象object2的原型链中,是则返回true,否则返回false。
Child.prototype.isPrototypeOf(child1) // true
Parent.prototype.isPrototypeOf(child1) // true
Object.prototype.isPrototypeOf(child1) // true
// 原型链继承优缺点
// 优点:继承了父类的模板,又继承了父类的原型对象
// 缺点:1. 如果要给子类的原型上新增属性和方法,就必须放在Child.prototype = new Parent()这样的语句后面
// 2. 无法实现多继承, 多个原型指向同一个实例,当有多个实例化子对象时,修改一个会影响其他对象
// 3. 来自原型对象的所有属性都被共享了【浅拷贝】
// 4. 创建子类时,无法向父类构造函数传参数
##### (2) 构造继承
function Parent (name) {
this.name = name
}
function Child () {
this.sex = ‘boy’
Parent.call(this, ‘child’) // 这一步操作相当于把Parent里边的this.name作为Child的属性并赋值, this.name = ‘child’
// 这里的call换成apply和bind也可以,同样的效果
}
var child1 = new Child()
console.log(child1) // {“sex”: “boy”, “name”: “child”}
// ===============================================================================================================
// 变种
function Parent (name) {
this.name = name
}
function Child () {
this.sex = ‘boy’
Parent.call(this, ‘good boy’) // 等同于 this.name = ‘good boy’
this.name = ‘bad boy’ // 相当于给this.name进行重新赋值
}
var child1 = new Child()
console.log(child1) // {sex: ‘boy’, name: ‘bad boy’}
// ===============================================================================================================
// 涉及引用类型
function Parent (name, sex) {
this.name = name
this.sex = sex
this.colors = [‘white’, ‘black’]
}
function Child (name, sex) {
Parent.call(this, name, sex)
}
var child1 = new Child(‘child1’, ‘boy’)
child1.colors.push(‘yellow’)
child1 // {“name”: “child1”, “sex”: “boy”, “colors”: [“white”, “black”, “yellow”]}
var child2 = new Child(‘child2’, ‘girl’)
child2 // {“name”: “child2”, “sex”: “girl”, “colors”: [“white”, “black”]}
// child1和child2可以看出,是一个类似深拷贝的过程
// ===============================================================================================================
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child () {
this.sex = ‘boy’
Parent.call(this, ‘good boy’)
}
Child.prototype.getSex = function () {
console.log(this.sex)
}
var child1 = new Child()
console.log(child1) // {sex: ‘boy’, name: ‘good boy’}
child1.getSex() // ‘boy’
child1.getName() // TypeError: child1.getName is not a function
// 缺点1:构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法
child1 instanceof Parent // fasle
// 缺点2:实例并不是父类的实例,只是子类的实例
##### (3) 组合继承(原型链继承 + 构造继承)
// 使用原型链继承来保证子类能继承到父类原型中的属性和方法;
// 使用构造继承来保证子类能继承到父类的实例属性和方法
function Parent (name) { // call()和new的时候会被调用两次产生无意义的内存开销, new后的实例中name值为undefined
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = ‘boy’
Parent.call(this, name) // 等同于this.name = name,使用call() 调用父类的属性(显式调用)
}
Child.prototype = new Parent() // 将Child.prototype 指向Parent的实例, 等同于Child.prototype.proto == Parent.prototype
Child.prototype.getSex = function () {
console.log(this.sex)
}
var child1 = new Child(‘child1’)
child1 // {sex: ‘boy’, name: ‘child1’}
// child1原型链:Child {sex: ‘boy’, name: ‘child1’}name : "child1"sex : “boy” [[Prototype]]:Parent getSex : ƒ ()name : undefined [[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
child1.getName() // child1 取的是Parent原型对象上的方法, 因为child1的原型对象指向了Parent实例
child1.getSex() // boy 取的是Child对象的原型对象Child的getSex方法
child1.constructor // f Parent (name) {this.name = name}
// 正常来将child1.constructor指向构造函数Child本身,由于Child.prototype的继承Parent实例,导致Child.prototype.constructor被切断,沿着原型链找,最终指向了Parent.prototype的constructor, 也就是Parent (name) {this.name = name}
// constructor 其实只是个标识作用,再实际的代码中并没有实际意义,所以是否在组合继承中修复这个地方取决于自己
Child.prototype.constrcutor = Child // 认亲爹
// ===============================================================================================================
// 如何修改隐式原型(可以修改,但是在任何时候都不建议去人工修改隐式原型)
// 经典调用案例
var a;
(function () {
function A () {
this.a = 1
this.b = 2
}
A.prototype.logA = function () {
console.log(this.a)
}
a = new A() // 实例化A, 赋值给全局window的a
})()
a.logA() // 1
// 如何在匿名函数外给A这个构造函数的原型对象中添加一个方法logB用以打印出this.b
a.constructor.prototype.logB = function() { console.log(this.b) }
a.logB() // 2 a.constructor指向A构造函数本身
// 用隐式原型也可以, a的__proto__指向A.prototype
a.proto.logB = function() { console.log(this.b) }
// ===============================================================================================================
function Parent (name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.features = [‘cute’]
function Child (name, colors) {
Parent.apply(this, [name, colors])
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
var child1 = new Child(‘child1’, [‘white’])
child1.colors.push(‘yellow’)
child1.features.push(‘sunshine’)
var child2 = new Child(‘child2’, [‘black’])
child1.colors // [‘cute’, ‘sunshine’]
child2.colors // [‘white’, ‘yellow’]
child1.features // [‘cute’, ‘sunshine’]
child2.features // [‘cute’, ‘sunshine’]
// features是定义在父类构造函数原型对象中的,是比new Parent()还要更深一层的对象;
// 它只能解决原型(匿名实例)中引用属性共享的问题。features是Parent.prototype上的属性,相当于是爷爷那一级
// 组合继承的优点
// 优点:
// 1.可以继承父类实例属性和方法,也能够继承父类原型属性和方法
// 2.弥补了原型链继承中引用属性共享的问题(注意原型的原型的属性依旧会共享给实例)
// 3.可传参,可复用
// 缺点:原型链继承和构造继承的时候都会调用一次
// 1.使用组合继承时,父类构造函数会被调用两次
// 2.并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。
##### (4) 寄生组合继承
* 核心:解决多次调用父类构造函数问题
// 1.Object.create(proto, propertiesObject) 创建一个新对象,新对象的原型链将指向指定的原型对象的方法。
// 参数一:需要指定的新对象的原型对象,即新对象通过原型链继承了原型对象的属性和方法
// 参数二:可选参数,给新对象自身添加新属性以及描述器
// 2.Object.setPrototypeOf(obj, prototype) 用于设置一个对象的原型的方法
// 参数一:要设置原型的对象
// 参数二:该对象的新原型
// 标准的寄生组合继承
// 使用 Object.create()
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = ‘boy’
Parent.call(this, name)
}
// 与组合继承的区别
Child.prototype = Object.create(Parent.prototype) // 创建了一个空对象,并且这个对象的__proto__属性是指向Parent.prototype
var child1 = new Child(‘child1’)
child1 // Child {sex: ‘boy’, name: ‘child1’}
child1.getName() // child1
child1.constructor // Parent (name) this.name = name 指向继承的Parent
console.log(child1.proto)
// 原型链:Parent {}[[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
// ===============================================================================================================
// 使用 Object.setPrototypeOf()
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = ‘boy’
Parent.call(this, name)
}
// 与组合继承的区别
Object.setPrototypeOf(Child.prototype, Parent.prototype) // 将Child的原型设置为Parent的对象
var child2 = new Child(‘child2’)
child2 // Child {sex: ‘boy’, name: ‘child1’}
child2.getName() // child2
child2.constructor // Child (name) {this.sex = 'boy’Parent.call(this, name)}
child2.proto
// 原型链:Parent {constructor: ƒ}constructor : ƒ Child(name) [[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
// 可以得出Object.setPrototypeOf()方法设置对象的原型更加完美一些,它修复了子类原型中constructor的指向问题
// ===============================================================================================================
// function Parent(name){
this.name = name
this.face = ‘cry’
this.colors = [‘white’, ‘black’]
}
Parent.prototype.features = [‘cute’]
Parent.prototype.getFeatures = function (){
console.log(this.features)
}
function Child(name){
Parent.call(this, name)
this.sex = ‘boy’
this.face = ‘smile’
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child1 = new Child(‘child1’)
child1.colors.push(‘yellow’)
var child2 = new Child(‘child2’)
child2.features = [‘sunshine’]
console.log(child1)
// 原型链:Child {name: ‘child1’, face: ‘smile’, colors: Array(3), sex: ‘boy’}colors : (3) [‘white’, ‘black’, ‘yellow’]face : "smile"name : "child1"sex : “boy” [[Prototype]]:Parent constructor : ƒ Child(name) [[Prototype]]:Object features : [‘cute’]getFeatures : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
console.log(child2)
// 原型链:Child {name: ‘child2’, face: ‘smile’, colors: Array(2), sex: ‘boy’, features: Array(1)}colors : (2) [‘white’, ‘black’]face : "smile"features : [‘sunshine’]name : "child2"sex : “boy” [[Prototype]]:Parent constructor : ƒ Child(name) [[Prototype]]:Object features : [‘cute’]getFeatures : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
// 寄生组合继承算是ES6之前一种比较完美的继承方式。
// 它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点
// 它拥有了原型链继承、构造继承和组合继承的所有继承方式的优点
// 只调用了一次父类构造函数,只创建了一份父类属性
// 子类可以用到父类原型链上的属性和方法
// 能够正常的使用instanceOf和isPrototypeOf方法
// Object.setPrototypeOf()方法会在运行时动态地改变对象的原型,这可能会对性能产生一些影响;
// 在创建对象时就应该使用Object.create()来设置原型链,而不是后期动态地改变原型链。
##### (5) 原型式继承
// 原理是创建一个构造函数,构造函数的原型指向对象,然后调用new 操作符创建实例,并返回这个实例,本质是一个浅拷贝
var cat = {
heart: ‘❤️’,
colors: [‘white’, ‘black’]
}
var guaiguai = Object.create(cat)
var huaihuai = Object.create(cat)
console.log(guaiguai)
// 原型链:{}[[Prototype]]:Object colors : (2) [‘white’, ‘black’]heart : “❤️” [[Prototype]]:Object
console.log(huaihuai)
// 原型链:{}[[Prototype]]:Object colors : (2) [‘white’, ‘black’]heart : “❤️” [[Prototype]]:Object
cat.colors.push(‘blue’) // 在原对象上个修改colors属性
guaiguai.colors // [‘white’, ‘black’, ‘blue’] 浅拷贝
// 同样的,对于 setPrototypeOf而言也是
var cat = {
heart: ‘❤️’,
colors: [‘white’, ‘black’]
}
res1 = {} // 因为Object.setPrototypeOf()没有返回值,所以先定义好一个对象
Object.setPrototypeOf(res1, cat); // res1的原型指向cat对象
##### (6) 寄生式继承
// 在原型式继承的基础之上进行了一下优化
var cat = {
heart: ‘❤️’,
colors: [‘white’, ‘black’]
}
function createAnother (original) { // 在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回。
var clone = Object.create(original);
clone.actingCute = function () {
console.log(‘我是一只会卖萌的猫咪’)
}
return clone;
}
var guaiguai = createAnother(cat)
var huaihuai = Object.create(cat)
huaihuai.heart // ‘❤️’
guaiguai.actingCute() // 我是一只会卖萌的猫咪
##### (7) 混入式继承(多继承)
// 我们一直都是以一个子类继承一个父类,而混入方式继承就是教我们如何一个子类继承多个父类的。
// 需要用到ES6中的方法Object.assign()
// 它的作用就是可以把多个对象的属性和方法拷贝到目标对象中,若是存在同名属性的话,后面的属性会把前面的覆盖掉
// Object.assign(target, …sources) 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象
// 参数一:需要应用源对象属性的目标对象,修改后将作为返回值
// 参数二:一个或多个包含要应用的属性的源对象
function Parent (sex) {
this.sex = sex
}
Parent.prototype.getSex = function () {
console.log(this.sex)
}
function OtherParent (colors) {
this.colors = colors
}
OtherParent.prototype.getColors = function () {
console.log(this.colors)
}
function Child (sex, colors) {
Parent.call(this, sex)
OtherParent.call(this, colors) // 新增的父类
this.name = ‘child’
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父类原型对象
Child.prototype.constructor = Child
var child1 = new Child(‘boy’, [‘white’])
console.log(child1)
// 原型链:Child {sex: ‘boy’, colors: Array(1), name: ‘child’}colors: [‘white’]name: "child"sex: “boy”[[Prototype]]: Parentconstructor: ƒ Child(sex, colors)getColors: ƒ ()arguments: nullcaller: nulllength: 0name: ""prototype: {constructor: ƒ}[[FunctionLocation]]: VM23262:10[[Prototype]]: ƒ ()[[Scopes]]: Scopes[1][[Prototype]]: ObjectgetSex: ƒ ()constructor: ƒ Parent(sex)[[Prototype]]: Object
child1.proto // Parent {getColors: ƒ, constructor: ƒ}
child1.proto.proto // {getSex: ƒ, constructor: ƒ}
// 可以看出Parent.prototype.getSex()和OtherParent.prototype.getColors(),他们不在同一个原型上, 正常来讲是在同一个原型上
// ===============================================================================================================
function Parent (sex) {
this.sex = sex
}
Parent.prototype.getSex = function () {
console.log(this.sex)
}
function OtherParent (colors) {
this.colors = colors
}
OtherParent.prototype.getColors = function () {
console.log(this.colors)
}
function Child (sex, colors) {
Parent.call(this, sex)
OtherParent.call(this, colors) // 新增的父类
this.name = ‘child’
}
Object.assign(Parent.prototype, OtherParent.prototype) // 新增的父类原型对象,其它原型上的属性添加的目标原型上
Child.prototype = Object.create(Parent.prototype) // 创建一个Parent的新对象,新对象指向Child的原型
// Object.setPrototypeOf(Child.prototype, Parent.prototype)
Child.prototype.constructor = Child // 认亲爹
var child1 = new Child(‘boy’, [‘white’])
console.log(child1)
// 原型链:Child {sex: ‘boy’, colors: Array(1), name: ‘child’}colors : [‘white’]name : "child"sex : “boy” [[Prototype]]:Parent constructor : ƒ Child(sex, colors) [[Prototype]]:Object getColors : ƒ ()getSex : ƒ ()constructor : ƒ Parent(sex) [[Prototype]]:Object
child1.proto.proto
// {getSex: ƒ, getColors: ƒ, constructor: ƒ} getSex和getColors现在已经在同一个原型上了
console.log(Child.prototype.proto === Parent.prototype) // true
console.log(Child.prototype.proto === OtherParent.prototype) // false
console.log(child1 instanceof Parent) // true
console.log(child1 instanceof OtherParent) // false
// 混入式继承的缺点:对于两个或者多个父类,只能维持一个instanceof正常。
##### (8) Class 继承
* 主要是依靠两个关键字:`extend`、`super`
class Parent {
constructor (name) {
this.name = name
}
getName () {
console.log(this.name)
}
}
class Child extends Parent { // class通过extends关键字实现继承,让子类继承父类的属性和方法
constructor (name) {
super(name) // super在这里表示父类的构造函数,用来新建一个父类的实例对象
this.sex = ‘boy’
}
}
var child1 = new Child(‘child1’)
console.log(child1)
// 原型链:Child {name: ‘child1’, sex: ‘boy’}name : "child1"sex : “boy” [[Prototype]]:Parent constructor : class Child[[Prototype]]:Object constructor : class Parent getName:ƒ getName() [[Prototype]]:Object
// 从完整的原型链可以看出class的继承方式完全满足于寄生组合继承
// ===============================================================================================================
class Parent {
constructor () {
this.name = ‘parent’
}
}
class Child extends Parent {
constructor () {
// super(name) // super注释掉
}
}
var child1 = new Child() // Uncaught ReferenceError: Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor
/*
【重点】
1.ES6规定,子类必须在constructor()方法中调用super(),否则就会报错。
这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,
然后再对其进行加工,添加子类自己的实例属性和方法。如果不先调用super()方法,子类就得不到自己的this对象。
2.如果使用了extends实现继承的子类内部没有constructor方法,则会被默认添加constructor和super;
若子类中有constructor方法,必须得在constructor中调用一下super函数,否则会报错。
4.需要注意的地方是,在子类的constructor方法中,只有调用super()之后,才可以使用this关键字,否则会报错。
这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。
可以理解为,新建子类实例时,父类的构造函数必定会先运行一次
*/
// ===============================================================================================================
// super关键字,既可以当作做函数使用,也可以当做对象使用。
// 1.super当成函数调用时,代表父类的构造函数,且返回的是子类的实例,也就是此时super内部的this指向子类
class Parent {
constructor () {
console.log(new.target.name) // 在构造函数内部使用new关键字创建对象时,返回该构造函数的名称。
}
}
class Child extends Parent {
constructor () {
var instance = super() // 在子类的constructor中super()就相当于是Parent.prototype.constructor.call(this)
/* 调用super()的作用是形成子类的this对象,把父类的实例属性和方法放到这个this对象上面;
子类在调用super()之前,是没有this对象的,任何对this的操作都要放在super()的后面。/
console.log(instance)
console.log(instance === this)
}
}
var child = new Child()
// Child
// Child {} super当做函数来使用时,虽然它代表着父类的构造函数,但是返回的却是子类的实例,也就是说super内部的this指向的是Child。
// true
/
super当做函数使用时的限制:
1.子类constructor中如果要使用this的话就必须放到super()之后
2.super当成函数调用时只能在子类的construtor中使用
*/
// super()在子类构造方法中执行时,子类的属性和方法还没有绑定到this,如果存在同名属性,此时拿到的是父类的属性。
class A {
name = ‘A’;
constructor() {
console.log(this.name);
}
}
class B extends A {
name = ‘B’;
}
new B(); // A 原因在于super()执行时,B的name属性还没有绑定到this,所以this.name拿到的是A的name属性
// super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
// 在普通方法中:
class A {
p() {
console.log(‘tzw’);
}
}
class B extends A {
constructor() {
super();
super.p(); // 2 就是将super当作一个对象使用, super.p()就相当于A.prototype.p()
// 需要注意的是由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
// 或者说定义在父类原型对象上的属性和方法,super就可以取到
}
}
new B();
// ES6规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
class A {
constructor() {
this.x = 1;
this.y = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
console.log(this.y) // 1
super.y = 2; // 由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
console.log(super.y) // undefined
// super.x赋值2,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined
console.log(this.y) // 2
}
m() {
super.print(); // 2 调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)
}
}
let b = new B();
b.m() // 2
// ===============================================================================================================
// 2.super作为对象时用在静态方法之中,这时super将指向父类;在普通方法之中指向父类的原型对象。
class Parent {
constructor (name) {
this.name = name
}
getName () {
console.log(this.name)
}
}
Parent.prototype.getSex = function () {
console.log(‘boy’) // child1
}
Parent.getColors = function () {
console.log([‘white’])
}
class Child extends Parent {
constructor (name) {
super(name)
super.getName()
// console.log(super); // 报错 要能清晰地表明super的数据类型(作为函数还是对象使用)才不会报错
}
instanceFn () {
super.getSex() // super在普通方法之中指向父类的原型对象
}
static staticFn () {
super.getColors() // 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
}
}
var child1 = new Child(‘child1’)
child1.instanceFn() // boy 在普通方法child1.instanceFn里面,super.getSex指向父类的原型对象
Child.staticFn() // [‘white’] 静态方法Child.staticFn里面,super.staticFn指向父类的静态方法。这个方法里面的this指向的是Child,而不是Child的实例。
//由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字
// extends后面接着的继承目标不一定要是个class,只要父类是一个有prototype属性的函数就能被子类继承
#### 3) 多态性
* 多态性是指具体多种形态或者实现方式,Java中的多态性允许类的子类定义它们自己的唯一行为,并且还共享父类的一些相同功能。
// 多态最根本的作用就是通过把过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
// 实际上是一种编程的思想
// 对于JS是具有与生俱来的多态性它的变量类型在运行期是可变的,程序并没有要求我指定它的类型,也就是它并不存在这种类型之间的耦合关系。
#### 4) 代理与反射
在JavaScript中通过代理和反射,开发人员可以更加灵活地操作和控制对象的行为,从而实现更高级的功能和自定义行为。
* 先了解什么是属性描述符?
>
> JavaScript中的属性描述符("Property Descriptor"本质上是一个JavaScript 普通对象)是用于描述一个属性的相关信息。每个属性都有一个相关联的属性描述符,它由以下几个属性组成:
>
>
> 1.`value`:属性的值。可以是任何有效的JavaScript值。默认为`undefined`。
>
>
> 2.`writable`:该属性是否可以被重新赋值存取器属性。如果设置为`true`,则属性的值可以被修改。默认为`true`。
>
>
> 注意:属性描述符中如果配置了 get 和 set 中的任何一个,则该属性不再是一个普通属性,而变成了存取器属性。
>
>
> get()读值函数:如果一个属性是存取器属性,则读取该属性时,会运行 get 方法,并将 get 方法得到的返回值作为属性值;
>
>
> set(newVal)存值函数:如果给该属性赋值,则会运行 set 方法,newVal 参数为赋值的值。
>
>
> \* value 和 writable 属性不能与 get 和 set 属性共存,二者只能选其一
>
>
> 存取器属性最大的意义,在于可以控制属性的读取和赋值,在函数里可以进行各种操作。
>
>
> 3.`enumerable`:表示属性是否可枚举。如果设置为`true`,则属性可以在`for..in`循环中被枚举。默认为`true`。
>
>
> 4.`configurable`:表示属性是否可配置。如果设置为`true`,则允许修改属性的描述符和删除属性。默认为`true`。
>
>
>
/*
1.获取对象属性描述符
Object.getOwnPropertyDescriptor(对象, 属性名) 获取一个对象的某个属性的属性描述符
Object.getOwnPropertyDescriptors(对象) 获取某个对象的所有属性描述符
2.为某个对象添加属性时 或 修改属性时,配置其属性描述符,使用以下这两种方法
Object.defineProperty(对象, 属性名, 描述符) 设置一个对象的某个属性
Object.defineProperties(对象, 多个属性的描述符); 设置一个对象的多个属性
需要注意的是,如果尝试修改不可配置的属性描述符,或者通过Object.defineProperty()
方法定义已经存在的属性,严格模式下会抛出错误,非严格模式下会被忽略。
*/
// 主要介绍下设置多个属性的描述符
var obj = {
property1: false,
property2: false,
};
Object.defineProperties(obj, {
‘property1’: {
value: true,
},
‘property2’: {
value: ‘Hello’,
enumerable: false, // 设置为不可枚举
}
});
for (let i in obj) { console.log(i) } // property1 property2不会被遍历,因为for…in遍历的是对象所有可遍历(enumerable)的属性
// ===============================================================================================================
// 描述符修改的几种情况
// 1.
let person1 = {};
Object.defineProperty(person1, “name”, {
configurable: true, // 允许修改属性的描述符和删除属性
writable: true, // 属性的值可以被修改
value: “abc”,
})
Object.defineProperty(person1, “name”, {writable: false}) // 第二次将writeable修改为第一次设置的相反值
console.log(person1.name); // abc
// 2.
let person2 = {};
Object.defineProperty(person2, “name”, {
configurable: true, // 允许修改属性的描述符和删除属性
writable: false, // 属性的值不可以被修改
value: “def”,
})
Object.defineProperty(person2, “name”, {writable: true}) // 第二次将writeable修改为第一次设置的相反值
console.log(person2.name); // abc
// 3.
let person3 = {};
Object.defineProperty(person3, “name”, {
configurable: false, // 不允许修改属性的描述符和删除属性
writable: true, // 属性的值可以被修改
value: “123”,
})
Object.defineProperty(person3, “name”, {writable: false}) // 第二次将writeable修改为第一次设置的相反值
console.log(person3.name); // 123
// 4.
let person4 = {};
Object.defineProperty(person4, “name”, {
configurable: false,
writable: false,
value: “456”,
})
Object.defineProperty(person4, “name”, {writable: true}) // 报错 TypeError: Cannot redefine property:
/*
1.当configurable为true 时,一切都是可以修改的
2.当configurable为false时,第二次修改为原值时不报错,如果第二次修改为相反值时存在一种特殊情况
configurable 与 writable 有一个值为true时value的值可修改 writable—>false
3.当configurable 为 false 时, 除了第2种情况之外,其它的修改都会报错。
----------------------------------------------------------------------------------------
enumerable 看 configurable
(configurable和enumerable:这两个选项之间没有直接的互斥关系,但它们与其他属性描述符选项存在相互关系)
value 看 configurable和writable
writable 看 configurable和writable
value和writable 与 get与set 互斥
get和set:如果同时设置了get和set选项,则不能再设置value或writable选项;
get和set方法提供了属性的自定义读取和写入行为,因此value和writable选项变得无意义。
*/
ES6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,\*\*可以给目标对象(target)定义一个关联的代理对象,而这个代理对象可当作一个抽象的目标对象来使用。\*\*因此在对目标对象的各种操作影响到目标对象之前,我们可以在代理对象中对这些操作加以控制,并且最终也可能会改变操作返回的结果。
##### (1) 代理
* 代理(Proxy)是一种机制,允许你创建一个代理对象来包装目标对象,并拦截该对象的操作。通过使用代理,你可以在目标对象上定义自定义的行为,例如拦截属性读取、属性设置、函数调用等操作。这使得你可以对对象的访问和修改进行更精细的控制。
* 代理(Proxy)能使我们开发者拥有一种间接修改底层方法的能力,从而控制用户的操作【在逆向中多用来进行hook】。
* 语法: let target = { /*目标对象的属性*/ }; //目标对象 let handler = { /*用来定制拦截操作*/ }; //拦截层对象 let proxy = new Proxy(target, handler); //实例化
const target = { // 目标对象
name: ‘John’
};
const handler = { // 处理器对象
// 捕获器在处理程序对象中以方法名为键
get(target, property, receiver) {
console.log(Reading property '${property}'
);
return target[property]; // 返回目标对象的属性值
},
set(target, property, value, receiver) {
console.log(Setting property '${property}' to '${value}'
);
target[property] = value; // 设置目标对象的属性值
return true; // 设置成功返回true
}
};
const proxy = new Proxy(target, handler); // 创建代理对象,代理对象使用Proxy构造函数将目标对象和处理程序对象handler结合起来
proxy.name // 查看代理对象的属性
// Reading property ‘name’
// john // get捕获器返回值
proxy.age = 26 // 修改代理对象设置属性
// Setting property ‘age’ to ‘26’
// 26 // set捕获器返回值
/*
常见的代理捕获方法:
1.get(target, property, receiver):捕获属性的读取操作。
2.set(target, property, value, receiver):捕获属性的赋值操作。
3.has(target, property):捕获in运算符的操作。
4.deleteProperty(target, property):捕获delete运算符的操作。
5.apply(target, thisArg, argumentsList):捕获函数调用的操作
*/
##### (2) 反射
* 反射(Reflection)是指通过内置的`Reflect`对象,提供一组方法来操作和查询对象的行为。Reflect 它提供了一系列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能;由于它类似于其他语言的反射,因此取名为 Reflect
* 使用 Reflect 可以实现诸如:属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中 等等功能。
const obj = {
name: ‘tzw’,
greeting() {
return Hello, ${this.name}!
;
}
};
console.log(Reflect.get(obj, ‘name’)); // tzw 获取obj对象的name属性
Reflect.set(obj, ‘age’, 26); // true 设置obj对象的age属性
console.log(Reflect.has(obj, ‘age’)); // true 查看obj对象是否有age属性
Reflect.deleteProperty(obj, ‘age’); // true 删除obj对象的age属性
console.log(obj.age) // undefined
const result = Reflect.apply(obj.greeting, { name: ‘qdd’ }, []); // 改变obj对象的greeting方法的this指向
console.log(result) // qdd
/*
常见的代理捕获方法
1.Reflect.get(target, property, receiver):获取对象的属性值。
2.Reflect.set(target, property, value, receiver):设置对象的属性值。
3.Reflect.has(target, property):检查对象是否具有指定属性。
4.Reflect.deleteProperty(target, property):删除对象的属性。
5.Reflect.apply(func, thisArg, args):调用函数并传递参数。
*/
// 典例
// eg1:(hook)
test6 = {a: 10}
var test6 = new Proxy(test6, {
defineProperty: function(target, property, descriptor) {
debugger;
console.log(arguments)
return Reflect.defineProperty(…argume nts)
}
});
attributes = {
configurable: false,
enumerable: false,
writable: false,
value: 1000,
}
Object.defineProperty(test6, ‘a’, attributes)
// eg2:
var proxy = {};
var target = proxy
var handler = {
getPrototypeOf: function (){console.log(arguments);return Reflect.getPrototypeOf(this)}
}
proxy1 = new Proxy(target, handler)
### 4. 异步
#### 1) 单线程模型
单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。 注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
#### 2) 同步任务和异步任务
程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。 **同步任务**是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。 **异步任务**是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
#### 3) 任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。 **首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。** JavaScript 引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。
#### 4) Event Loop 事件循环机制
浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
>
> event loop 执行顺序: 1.一开始整个脚本作为一个宏任务执行(可以是一个js脚本,或者script标签) 2.执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列 3.当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完 4.执行浏览器UI线程的渲染工作 5.检查是否有Web Worker任务,有则执行 6.执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
>
>
>
**什么是宏任务队列、微任务队列?** 宏任务队列,也叫宏队列:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering 微任务队列,也叫微队列:MutationObserver、Promise.then()或catch()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收过程、 async 、Node独有的process.nextTick等。 需要重点关注的是:setTimeout、setInterval 、Promise.then、 async **在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。**
#### 5) Promise对象
* Promise(是一个对象,也是一个构造函数)是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。
* Promise 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
* Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。
* Promise 实例有一个then方法,用来指定下一步的回调函数。
##### (1) Promis的用法
// 创建一个异步对象
var promise = new Promise((resolve, reject) => { // 回调函数写成更简介的箭头函数,是一个标准的promise
// …
if (/* 异步操作成功 /){
resolve(value);
} else { / 异步操作失败 /
reject(new Error());
}
});
/
1.Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
2.resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
3. reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
*/
>
> Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态:
>
>
> * 异步操作未完成(pending)
> * 异步操作成功(fulfilled)
> * 异步操作失败(rejected)
>
>
> 这三种状态里边,fulfilled 和rejected 合在一起称为resolved(已定型)。 这三种的状态的变化途径只有两种:
>
>
> * 从“未完成”到“成功 ”
> * 从“未完成”到“失败”
>
>
> 一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,**Promise 实例的状态变化只可能发生一次。**
>
>
> 所以,Promise 的最终结果只有两种:
>
>
> * 异步操作成功,Promise 实例传回一个值(value),状态变为`fulfilled`。
> * 异步操作失败,Promise 实例抛出一个错误(error),状态变为`rejected`。
>
>
>
// 初始态
var callback = function(resolve, reject){}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {}
// 成功态
var callback = function(resolve, reject){resolve()}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {: undefined}
// 失败态
var callback = function(resolve, reject){reject()}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {: undefined}
// 以上就是promise1的三种状态
// 打印顺序是什么?
const promise1 = new Promise((resolve, reject) => { // Promise 不是一个异步操作
console.log(‘promise1’)
})
console.log(‘1’, promise1);
// promise1
// 1 Promise {}
/*
创建promis对象的时候,此时,它是一个同步操作,promise1.then()才是一个异步操作;
所以,执行顺序从上至下,先遇到new Promise,执行该构造函数中的代码,打印promise1;
然后执行同步代码打印1,此时promise1没有被resolve或者reject,因此状态还是pending。
/
/
promise1 原型:Promise.prototype
constructor: ƒ Promise() 这玩意没啥说的
then: ƒ then() 主要看这个
catch: ƒ catch() 看名字就应该知道它应该是错误回收
finally: ƒ finally() 看名字就应该知道它是最后执行的
*/
##### (2) Promise.then
* Promise 实例的`then`方法,用来添加回调函数。
// 1. promise是成功态时
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve(‘success’)
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
console.log(promise);
// 1
// 2
// 4
// Promise {: ‘success’}
// 3
/*
执行步骤:
1.从上至下,先遇到new Promise,执行其中的同步代码打印1
2.再遇到resolve(‘success’), 将promise的状态改为了fulfilled(成功态)并且将值保存下来
3.继续执行同步代码打印2
4.跳出promise对象,往下执行,碰到promise.then这个微任务,将其加入微任务队列【注意,这里实际上发生的过程】
5.执行同步代码打印4
6.执行同步代码promise,打印promise的状态
7.本轮宏任务队列全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为fulfilled,执行它。
*注意:
在看到promise.then()还会创建一个promis对象,它的状态为pending(初始态);
当then发方法里边的函数执行完且没有报错的情况下,它的状态变为fulfilled(成功态);
如果报错,就会变成rejected(失败态)。
*/
// 2. promise是失败态时
const promise = new Promise((resolve, reject) => {
console.log(1);
reject(‘failed’)
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
// 1
// 2
// 4
/*
执行步骤:
1.从上至下,先遇到new Promise,执行其中的同步代码打印1
2.再遇到reject(‘failed’), 将promise的状态改为了rejected(失败态)并且将值保存下来
3.继续执行同步代码打印2
4.跳出promise对象,往下执行,碰到promise.then,只有promise是成功态的时候,promise.then里面的内容才会被放到微任务队列里边,
5.执行同步代码打印4
6.执行同步代码promise,打印promise的状态
7.本轮宏任务队列全部执行完毕,检查微任务队列,没有任务。
*注意:
promise.then(), 只有promise是fulfilled(成功态)的时候,promise.then里边的内容才会被放到微任务队列
*/
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => { // 此时的promise的状态是pending,不会放到微任务队列
console.log(3);
});
console.log(4);
// 1
// 2
// 4
* `then`方法可以接受两个回调函数,第一个是异步操作成功时(变为`fulfilled`状态)的回调函数,第二个是异步操作失败(变为`rejected`)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
const then_callback = function(){console.log(arguments)}
const promise = new Promise((resolve, reject) => {
resolve(‘success’) // 这个成功态的值会被promise.then的回调函数所接收到,它的第一个参数就是这个值
});
const result = promise.then(then_callback);
console.log(result);
// Promise {} 当一个已完成的“F”状态的Promise遇到then方法,一定瞬间产生一个新的Promise,状态为P
// { “0”: “success” }
const promise1 = new Promise((resolve, reject) => {
console.log(‘promise1’)
resolve(‘resolve1’)
})
const promise2 = promise1.then(res => { //promise1调用then的时候,不管后面跟的什么,一瞬间创建一个新的Promise对象
console.log(res)
})
console.log(‘1’, promise1);
console.log(‘2’, promise2);
// promise1
// 1 Promise {: ‘resolve1’}
// 2 Promise {}
// resolve1
/*
执行步骤:
1.从上至下,先遇到new Promise,执行该构造函数中的代码打印promise1
2.碰到resolve函数, 将promise1的状态改变为fullfiled,并将结果保存下来
3.碰到promise1.then这个微任务,将它放入微任务队列【只要遇到then方法,一定产生一个新的Promise】
4.promise2是一个新的状态为pending的Promise
5.执行同步代码打印1, 同时打印出promise1的状态是fulfilled
6.执行同步代码打印2,同时打印出promise2的状态是pending
8.宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为fulfilled,执行它。
*/
const fn = () =>
new Promise((resolve, reject) => {
console.log(1);
resolve(“success”);
});
console.log(“start”);
fn().then(res => {
console.log(res);
});
// start
// 1
// success
* 对于 Promise这个构造函数,如果不去new 它的话
const promise1 = Promise.resolve(‘success’) // 产生了一个已完成的 Promise
const promise2 = promise1.then((res) => {console.log(res)})
console.log(promise2)
// Promise {}
// success
// 在控制台再次打印
console.log(promise2)
// Promise {: undefined}
/*
可以得出结论:promise1.then的返回值是promise实例化对象【已完成】,当.then的一瞬间,创造一个状态为Pending的promise,如果之前的promise已完成,就一瞬间把回调函数放入微队列;这个promise返回值的状态是 pending,但是一旦微队列里面的内容执行完毕了,那么,这个值就变成了fulfilled状态
*/
// ===============================================================================================================
// 采用链式的then,可以指定一组按照次序调用的回调函数;
// 前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),
// 这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
// 经典案列:
Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
}).then(
(res) => {
console.log(res)
}
)
Promise.resolve()
.then(
() => {
console.log(1);
}).then(
() => {
console.log(2);
}).then(
() => {
console.log(3);
}).then(
() => {
console.log(5);
}).then(
() => {
console.log(6);
})
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 解析参考:https://www.zhihu.com/question/453677175?rf=512445784
#### 6) Promise结合setTimeout
* 根据Event Loop 事件机制,setTimeout 会生成一个宏任务队列
console.log(‘start’)
setTimeout(() => {
console.log(‘time’)
})
//.then方法接收两个回调函数,当Promise是成功态时把第一个回调函数放到微任务队列里边去,失败态时将第二个回调函数放到微任务队列里边去。
Promise.resolve().then(() => {
console.log(‘resolve’)
})
console.log(‘end’)
// start
// end
// resolve
// time
/*
执行步骤:
1.刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈进行执行,因此先打印出start和end。
2.setTimout作为一个宏任务被放入宏任务队列(下一个宏任务队列,并不是当前宏任务队列)
3.Promise.then作为一个微任务,因为是成功态,所以被放入微任务队列
4.本次宏任务执行完,检查微任务,发现Promise.then,执行它
5.接下来进入下一个宏任务,发现setTimeout,执行。
*/
// 当我们new一个Promise的时候它是一个同步执行的任务,
// 只有调用.then方法的时候才会把then里边的回调函数放到微任务队列里边去
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log(“timerStart”);
resolve(“success”);
console.log(“timerEnd”);
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
// 1
// 2
// 4
// timerStart
// timerEnd
// success
/*
执行步骤:
1.从上至下,先遇到new Promise,执行该构造函数中的代码1
2.然后碰到了定时器,将这个定时器中的函数放到下一个宏任务的延迟队列中等待执行
3.执行同步代码2
4.跳出promise函数,遇到promise.then,但其状态还是为pending,这里理解为先不执行
5.执行同步代码4
6.一轮循环过后,进入第二次宏任务,发现延迟队列中有setTimeout定时器,执行它
7.首先执行timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列
8.继续执行同步代码timerEnd
9.宏任务全部执行完毕,查找微任务队列,发现promise.then这个微任务,执行它。
*/
// ===============================================================================================================
// *宏任务队列中产生的微任务会在当前宏任务完成后立即执行
setTimeout(() => {
console.log(‘timer1’);
Promise.resolve().then(() => {
console.log(‘promise’)
})
}, 0)
setTimeout(() => {
console.log(‘timer2’)
}, 0)
console.log(‘start’)
// start
// timer1
// promise
// timer2
// 当宏任务执行过程中产生微任务的时候,会直接轮询微任务;
// 也可以理解为setTimeout会独立启用一个宏任务队列,而不是去排队。
Promise.resolve().then(() => {
console.log(‘promise1’);
const timer2 = setTimeout(() => {
console.log(‘timer2’)
}, 0)
});
const timer1 = setTimeout(() => {
console.log(‘timer1’)
Promise.resolve().then(() => {
console.log(‘promise2’)
})
}, 0)
console.log(‘start’);
// start
// promise1
// timer1
// promise2
// timer2
/*
执行过程:
1.刚开始整个脚本作为第一次宏任务来执行,我们将它标记为宏1,从上至下执行
2.遇到Promise.resolve().then这个微任务,将then中的内容加入第一次的微任务队列标记为微1
3.遇到定时器timer1,将它加入下一次宏任务的延迟列表,标记为宏2,等待执行(先不管里面是什么内容)
4.执行宏1中的同步代码打印start
5.第一次宏任务(宏1)执行完毕,检查第一次的微任务队列(微1),发现有一个promise.then这个微任务需要执行
6.执行打印出微1中同步代码promise1,然后发现定时器timer2,将它加入宏任务队列宏2的后面,标记为宏3
7.第一次微任务队列(微1)执行完毕,执行第二次宏任务(宏2),首先执行同步代码打印timer1
8.然后又遇到了Promise.resolve().then这个微任务假定它是promise2,将它加入此次循环的微任务队列,标记为微2
9.宏2中没有同步代码可执行了,查找本次循环的微任务队列(微2),发现了promise2,执行它
10.第二轮执行完毕,执行宏3,打印出timer2
*注意:setTimeout 会独立启动一个宏任务队列,而不是去当前宏任务队列去排队。
*/
// Promise.resolve().then 产生的微任务会进入本轮微任务队列, 而不会进入下一轮
Promise.resolve().then(() => {
console.log(‘promise1’);
const timer2 = setTimeout(() => {
console.log(‘timer2’)
}, 0)
});
const timer1 = setTimeout(() => {
console.log(‘timer1’)
Promise.resolve().then(() => {
console.log(‘promise2’)
})
}, 0)
Promise.resolve().then(() => {
console.log(‘promise3’);
const timer3 = setTimeout(() => {
console.log(‘timer3’)
Promise.resolve().then(() => {console.log(‘promise4’)})
}, 0);
const timer4 = setTimeout(() => {
console.log(‘timer4’)
}, 0);
});
console.log(‘start’);
// start
// promise1
// promise3
// timer1
// promise2
// timer2
// timer3
// promise4
// timer4
// ===============================================================================================================
// setTimeout加上延时
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(‘success’)
}, 1000) // 注意这个延时
})
const promise2 = promise1.then(() => {
throw new Error(‘error!!!’) // thrw抛出报错,new Error不会报错,只是生成一个错位的对象
})
console.log(‘promise1’, promise1)
console.log(‘promise2’, promise2)
setTimeout(() => {
console.log(‘promise1’, promise1)
console.log(‘promise2’, promise2)
}, 0)
// promise1 Promise {}
// promise2 Promise {}
// Uncaught (in promise) Error: error!!!
// promise1 {: ‘success’}
// promise2 Promise {: Error: error!!!}
/*
throw 语句用来抛出一个用户自定义的异常。
当前函数的执行将被停止(throw之后的语句将不会执行),并且控制将被传递到调用堆栈中的第一个catch块。
如果调用者函数中没有catch块,程序将会终止。
*/
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(“success”);
console.log(“timer1”);
}, 1000); // 注意这个时间
console.log(“promise1里的内容”);
});
const promise2 = promise1.then(() => {
throw new Error(“error!!!”);
});
console.log(“promise1”, promise1);
console.log(“promise2”, promise2);
setTimeout(() => {
console.log(“timer2”);
console.log(“promise1”, promise1);
console.log(“promise2”, promise2);
}, 0); // 注意这个时间
// promise1里的内容
// promise1 {}
// promise2 {}
// timer2
// promise1 {}
// promise2 {}
// timer1 // timer2比timer1先打印,原因在于setTimeout执行的定时器时间
// Uncaught (in promise) Error: error!!! 输出的时候,报错的优先级要小于日志的优先级
/*
setTimeout解释:
当第一个 setTimeout() 被调用时,它会将回调函数推入异步执行队列中,并设定一个定时器,经过指定的时间后将该回调函数移出队列并执行。
如果在定时器到期之前有其他的代码被执行,则该定时器会被暂时挂起,待其他的代码执行完毕后再继续执行这个定时器。
当当前的执行栈为空时,JavaScript 引擎会检查异步执行队列,并按照队列的先后顺序执行其中的回调函数;
对于多个 setTimeout() 的情况,引擎会按照设定的时间间隔依次执行它们的回调函数。
需要注意的是,具体的执行时间可能会受到一些因素的影响,如系统负载、浏览器的事件循环机制等。【当某个定时器的时间到期时,如果当前的执行栈仍然有其他任务在执行,JavaScript 引擎会等待执行栈为空后才会处理定时器回调函数。】因此,设置多个定时器时,它们会按照设定的时间间隔依次添加到异步执行队列中,但具体的执行时间会受到其他因素的影响。并不能保证它们会严格按照设定的时间间隔依次执行,而是在当前执行栈为空时,尽可能快速地按照先后顺序执行。
*/
#### 7) Promise A+ 规范
* [www.ituring.com.cn/article/665…](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)
// 1
const promise = new Promise((resolve, reject) => {
resolve(“success1”);
reject(“error”); // 没有任何作用,因为Promise的状态一旦改变,就永久保持该状态,不会再变了
resolve(“success2”); // 没有任何作用,因为Promise的状态一旦改变,就永久保持该状态,不会再变了
});
promise.then(res => {
console.log("then: ", res);
}).catch(err => { // promise抛出一个错误,就被catch()方法指定的回调函数捕获。
console.log("catch: ", err);
})
// then: success1
// 总结:promise只要状态从P转变成 F/R,状态就不会再次被改变
// 2
let P1 = Promise.reject().then(()=>{})
console.log(P1)
// Promise {} Promise.then状态为pending
// Uncaught (in promise) undefined 这个报错是Promise.reject()产生的,输出时日志的优先级高于报错的优先级
console.log(P1) // 再次打印
// Promise {: undefined} then方法的第二个参数是rejected状态的回调函数,由于没有第二个回调函数所以是undefined
// 总结:rejected状态的Promis.then方法没有第二个参数,就会返回rejected状态, 如果有第二个参数会返回fulfilled状态
// 3
let P2 = Promise.resolve().then(undefined,()=>{console.log(1)})
console.log(P2)
// Promise {} // Promise.then状态为pending
console.log(P2) // 再次打印
// 总结:fulfilled状态的Promis.then方法没有第一个参数,就会返回fulfilled状态
// 4
let P3 = Promise.reject().then(()=>{}, ()=>{})
console.log(P3)
// Promise {}
console.log(P3) // 再次打印
// Promise {: undefined}
/*
说一下为什么第二次打印状态变为fulfiled或者rejected:
1. 是因为 Promise 的状态改变和回调函数的执行是异步的;
2. 当调用 Promise 的方法(比如 resolve()、reject())时,Promise 对象的状态并不会立即改变,而是在 JavaScript 的事件循环机制中等待下一个微任务阶段执行。
3. 如果在调用 Promise 方法后立即打印 Promise 对象的状态,那么很可能此时 Promise 对象的状态尚未改变,仍然是 “pending”。
4. 只有在后续的微任务阶段中,JavaScript 引擎会检查微任务队列中是否有待处理的回调函数,并依次执行这些函数。这时才会执行 Promise 对象的回调函数,并且状态可能被改变为 “fulfilled” 或 “rejected”。
小tip: 想要准确获取 Promise 对象的最新状态,可以在 then() 方法注册的回调函数中进行状态的打印,这样可以确保你在 Promise 状态变为 “fulfilled” 或 “rejected” 之后再打印对应的状态。
*/
// 5
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch()
// 报错
// 总结:.then不能返回其本身,否则报错。 这两句话都报错,但是promise的返回值还是可以接收到的。
// 再次印证了观点:promise.then出现的一瞬间,就会出现一个新的promise而不会去管.then的回调函数的内容。
#### 8) Prmise.catch
* `Promise.prototype.catch()`方法是`.then(null, rejection)`或`.then(undefined, rejection)`的别名,用于指定发生错误时的回调函数。
* `then()`方法指定的回调函数,如果运行中抛出错误,也会被`catch()`方法捕获。
const promise = new Promise((resolve, reject) => {
reject(“error”);
resolve(“success2”); // 没有任何作用,因为Promise的状态一旦改变,就永久保持该状态,不会再变了
});
promise.then(res => {
console.log("then1: ", res);
}).then(res => {
console.log("then2: ", res);
}).catch(err => { // catch可以捕捉到任何时候上层还没有捕捉的错误
console.log("catch: ", err);
}).then(res => {
console.log("then3: ", res);
})
// catch: error
// then3: undefined
/*
执行步骤:
1.由于new的Promise对象,暂定为P0的状态是一个rejected(拒绝态),拒因是 error;
2.调用第一个then方法,由于P0的状态已经确定为rejected,then方法没有第二参数,所以当前Promise.then状态为rejected 记为P1;
3.调用第二个then方法,then方法还是没有第二参数,所以当前Promise.then状态为rejected 记为P2;
4.之后遇到catch方法,此时Promise.catch的状态是pending,由于P2是rejected,所以就捕获到了错误,并接收到拒因,控制台打印拒因,此时状态就会变为fulfilled 记为C1;
5.调用最后一个then方法时,由于C1的状态为fulfilled, 所以执行then方法的第一个参数,由于C1没有传值,所以控制台打印是undefined
*/
// 根据以上的案列是否可以理解为,then方法实际上是包含错误捕捉功能的,then方法的第二个参数实际上就是 catch
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
HTTP
-
HTTP 报文结构是怎样的?
-
HTTP有哪些请求方法?
-
GET 和 POST 有什么区别?
-
如何理解 URI?
-
如何理解 HTTP 状态码?
-
简要概括一下 HTTP 的特点?HTTP 有哪些缺点?
-
对 Accept 系列字段了解多少?
-
对于定长和不定长的数据,HTTP 是怎么传输的?
-
HTTP 如何处理大文件的传输?
-
HTTP 中如何处理表单数据的提交?
-
HTTP1.1 如何解决 HTTP 的队头阻塞问题?
-
对 Cookie 了解多少?
-
如何理解 HTTP 代理?
-
如何理解 HTTP 缓存及缓存代理?
-
为什么产生代理缓存?
-
源服务器的缓存控制
-
客户端的缓存控制
-
什么是跨域?浏览器如何拦截响应?如何解决?