目录
1.JavaScript闭包
1)闭包概念
2)闭包的注意点
2. this
1) 关键点
2) 四类调用方式
3.JS宏任务和微任务
1)什么是微任务和宏任务
2)JS为什么要区分微任务和宏任务
3)微任务和宏任务有哪些
4)微任务和宏任务是怎么执行
4.对象的继承
1)prototype 属性的作用
2)原型链
3)constructor 属性
4)构造函数的继承
5)多重继承
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。—引自MDN
在JS中,通俗来讲,闭包就是能够读取外层函数内部变量的函数。
变量的作用域为两种:全局作用域和局部作用域
1)函数内部可以读取全局变量
let code = 200;
function f1() {
console.log(code);
}
f1(); // 200
2)函数外部无法读取函数内部的局部变量
function f1() {
let code = 200;
function f2() {
console.log(code);
}
}
闭包访问的变量,是每次运行上层函数时重新创建的,是相互独立的。
function f1() {
let obj = {};
function f2() {
return obj;
}
return f2;
}
let result1 = f1();
let result2 = f1();
console.log(result1() === result2()); // false
不同的闭包,可以共享上层函数中的局部变量
function f() {
let num = 0;
function f1() {
console.log(++num);
}
function f2() {
console.log(++num);
}
return {f1,
f2};
}
let result = f();
result.f1(); // 1
result.f2(); // 2
旅行者走路的问题
function factory() {
var start = 0
function walk(step) {
var new_total = start + step
start = new_total
return start
}
return walk
}
var res = factory()
res(1)
res(2)
res(3)
1.由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2.闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值
this始终指向调用该函数的对象;
若没有指明调用的对象,则顺着作用域链向上查找,最顶层为global(window)对象;
箭头函数中的this是定义函数时绑定的,与执行上下文有关
简单对象(非函数、非类)没有执行上下文;
类中的this,始终指向该实例对象;
箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。
1.作为对象方法的调用
function f() {
console.log( this.code );
}
let obj = {
code: 200,
f: f
};
obj.f(); // 200
2.纯粹的函数调用
function f() {
console.log( this.code );
}
// 此处,通过var(函数作用域)声明的变量code会绑定到window上;如果使用let(块作用域)声明变量code,则不会绑定到window上,因此下面的2次函数调用f(),会输出undefined
// let code = 200;
var code = 200;
f(); // 200
code = 404;
f(); // 404
3.作为[构造函数]调用
code = 404
function A() {
this.code = 200
this.callA = function() {
console.log(this.code)
}
}
var a = new A()
a.callA() // 200, callA在new A返回的对象里
4.使用apply、call、bind调用
(1)apply
var code = 404;
let obj = {
code: 200,
f: function() {
console.log(this.code);
}
}
obj.f(); // 200, 实际上是作为对象的方法调用
obj.f.apply(); // 404,参数为空时,默认使用全局对象global,在此处为对象window
obj.f.apply(obj); //200,this指向参数中设置的对象
(2)call
function f() {
console.log( this.code );
}
var obj = {
code: 200
};
f.call( obj ); // 200
(3)bind
// bind返回一个新的函数
function f(b) {
console.log(this.a, b);
return this.a + b;
}
var obj = {
a: 2
};
var newF = f.bind(obj);
var result = newF(3); // 2 3
console.log(result); // 5
首先,我们要先了解下 Js 。js 是一种单线程语言,简单的说就是:只有一条通道,那么在任务多的情况下,就会出现拥挤的情况,这种情况下就产生了 ‘多线程’ ,但是这种“多线程”是通过单线程模仿的,也就是假的。那么就产生了同步任务和异步任务。
(1)js是单线程的,但是分同步异步
(2)微任务和宏任务皆为异步任务,它们都属于一个队列
(3)宏任务一般是:script、setTimeout、setInterval、postMessage
(4)微任务:Promise.then ES
(5)先执行同步再执行异步,异步遇到微任务,先执行微任务,执行完后如果没有微任务,就执行下一个宏任务,如果有微任务,就按顺序一个一个执行微任务
(1)宏任务一般是:script、setTimeout、setInterval、postMessage
(2)微任务:Promise.then
4执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
这里容易产生一个错误的认识:就是微任务先于宏任务执行。实际上是先执行同步任务然后在执行异步任务,异步任务是分宏任务和微任务两种的。
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现,本章介绍 JavaScript 的原型链继承。
ES6 引入了 class 语法,基于 class 的继承不在这个教程介绍,请参阅《ES6 标准入门》一书的相关章节
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。
下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype
属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
上面代码中,函数f
默认具有prototype
属性,指向一个对象。
对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
构造函数的一个方法,或者一个属性
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
上面代码中,构造函数Animal
的prototype
属性,就是实例对象cat1
和cat2
的原型对象。原型对象上添加一个color
属性,结果,实例对象都共享了该属性。
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
a---->b------>c------>d-------->Object
如果一层层地上溯,所有对象的原型最终都可以上溯到
Object.prototype
,即Object
构造函数的prototype
属性。也就是说,所有对象都继承了Object.prototype
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。那么,
Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null
。
Object.getPrototypeOf(Object.prototype)
// null
prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
由于constructor
属性定义在prototype
对象上面,意味着可以被所有实例对象继承。
function P() {}
var p = new P();
//应该有这个属性吗
p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
上面代码中,p
是构造函数P
的实例对象,但是p
自身没有constructor
属性,该属性其实是读取原型链上面的P.prototype.constructor
属性。
让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。
function Sub(value) {
Super.call(this);
this.prop = value;
}
上面代码中,Sub
是子类的构造函数,this
是子类的实例。在实例上调用父类的构造函数Super
,就会让子类实例具有父类实例的属性。
第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
上面代码中,Sub.prototype
是子类的原型,要将它赋值为Object.create(Super.prototype)
,而不是直接等于Super.prototype
。否则后面两行对Sub.prototype
的操作,会连父类的原型Super.prototype
一起修改掉。
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'
上面代码中,子类S
同时继承了父类M1
和M2
。这种模式又称为 Mixin(混入)。