目录
一、对象和构造函数
1.对象是什么
2.构造函数
二、This指向以及绑定方法
1.This指向
1.1.This的含义
1.2.使用场景
2.绑定This方法
Function.prototype.call()
Function.prototype.apply()
Function.prototype.bind()
三、闭包
1.概念
2.使用闭包的注意点
四、同步异步
1.同步
2.异步
五、宏任务与微任务
1.为什么要区分宏任务与微任务
2.宏任务
3.微任务
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
那么,“对象”(object)到底是什么?我们从两个层次来理解。
(1)对象是单个实物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2)对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal
对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。
典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。
JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
构造函数就是一个普通的函数,但具有自己的特征和用法。
构造函数的特点有两个。
函数体内部使用了this
关键字,代表了所要生成的对象实例。
生成对象的时候,必须使用new
命令。
示例1:
var Vehicle = function () {
this.price = 1000;
};
var v = new Vehicle(); //使用new命令新生成一个实例v
v.price // 1000
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
前一章已经提到,this
可以用在构造函数之中,表示实例对象。除此之外,this
还可以用在别的场合。但不管是什么场合,this
都有一个共同点:它总是返回一个对象。
简单说,this
就是属性或方法“当前”所在的对象。
示例2:
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var name = '李四';
var f = A.describe;
console.info(f()) // "姓名:李四"
console.info(A.describe())// "姓名:张三"
上面代码中,在window下定义了name='李四',又将A的describe方法赋给了f变量,然后直接在window下调用f(),此时this就会指向顶层window,即window.name,也就是'李四'。
而在A类中调用describe方法,this便会指向A,即A.name,也就是'张三'。
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this
的指向是可变的。
(1)全局环境
全局环境使用this
,它指的就是顶层对象window
。也就是上面的示例2中的f()函数
(2)构造函数
构造函数中的this
,指的是实例对象。也就是上面的示例1中的this.price
(3)对象的方法
如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
示例3:
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
上面代码中,第一个obj.foo
方法执行时,它内部的this
指向obj
。但有上述三种情况,this指向window。
上面代码中,obj.foo
就是一个值。这个值真正调用的时候,运行环境已经不是obj
了,而是全局环境,所以this
不再指向obj
。
可以这样理解,JavaScript 引擎内部,obj
和obj.foo
储存在两个内存地址,称为地址一和地址二。obj.foo()
这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this
指向obj
。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this
指向全局环境。
(4)箭头函数
箭头函数中的this是定义函数时绑定的,而不是在执行函数时绑定。若箭头函数在简单对象中,由于简单对象没有执行上下文,所以this指向上层的执行上下文;若箭头函数在函数、类等有执行上下文的环境中,则this指向当前函数、类。
var code = 404;
let obj = {
code: 200,
getCode: () => {
console.log(this.code);
}
}
obj.getCode(); // 404
在箭头函数中,this 的值是在定义函数时确定的,而不是在运行时确定的。在这个例子中,箭头函数 getCode 是在对象 obj 定义时创建的,而不是在调用 obj.getCode() 的时候。
箭头函数中的 this 指向的是外层的词法作用域的 this 值,而不是指向调用它的对象。在全局作用域中,this 指向的是全局对象(在浏览器环境中通常是 window 对象)。所以,当箭头函数中使用 this.code 时,它实际上是引用全局作用域中的 code 变量,其值为 404。
由于obj普通对象是在全局下定义的,this指向window
this
的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this
固定下来,避免出现意想不到的情况。JavaScript 提供了call
、apply
、bind
这三个方法,来切换/固定this
的指向。
func.call(thisValue, [arg1], [arg2], [...])
函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数
示例4:
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
上面代码中,全局环境运行函数f
时,this
指向全局环境(浏览器为window
对象);call
方法可以改变this
的指向,指定this
指向对象obj
,然后在对象obj
的作用域中运行函数f
。
call
方法的参数,应该是一个对象。如果参数为空、null
和undefined
,则默认传入全局对象。
如果call
方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call
方法。
func.apply(thisValue, [arg1, arg2, ...])
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数
func.call(thisValue, [arg1], [arg2], [...])
bind()
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
示例5:
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var name = '李四';
var f = A.describe;
console.info(f()) // "姓名:李四"
console.info(A.describe())// "姓名:张三"
var pname=A.describe.bind(A)
console.info(pname()) // "姓名:张三"
我们沿用上面的示例,我们使用f变量相同的方式将A.describe方法赋给了pname变量,然后直接调用pname(),但与f变量不同的是,这次我们添加了bind方法,将this指向固定在A中,此时调用pname()函数,返回的仍然是'张三'。
bind()
还可以接受更多的参数,将这些参数绑定原函数的参数。
示例6:
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
上面代码中,bind()
方法除了绑定this
对象,还将add()
函数的第一个参数x
绑定成5
,然后返回一个新函数newAdd()
,这个函数只要再接受一个参数y
就能运行了。
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
在JS中,通俗来讲,闭包就是能够读取外层函数内部变量的函数。
函数内部可以读取全局变量,函数外部无法读取函数内部的局部变量
示例7:
a=400
function f1() {
let code = 200;
function f2() {
console.log(code);
}
return f2;
}
function f3(){
console.log(a)
}
console.info(f3()) //400,函数内部可以读取全局变量
console.info(code) //undefined,函数外部无法读取函数内部的局部变量
console.info(f1()()) // 200,函数f1内部的函数f2可以读取f1中所有的局部变量。因此,若想在外部访问
//函数f1中的局部变量`code`,可通过函数f2间接访问。
1.2中的函数f2,就是闭包,其作用就是将函数内部与函数外部进行连接。
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
同步(Synchronous): 在同步代码执行中,代码会按照从上到下的顺序依次执行,每一行代码执行完毕后才会继续执行下一行代码。在同步执行中,如果某个操作耗时较长(比如网络请求或文件读取),整个代码执行会被阻塞,直到该操作完成,才会执行下一行代码。这种执行方式使得代码简单易读,但也可能导致程序在长时间等待I/O操作时出现停滞,降低用户体验。
示例8:
console.log("Start");
console.log("Step 1");
console.log("Step 2");
console.log("End");
上述代码中的四个console.log
语句将按照顺序依次执行,并且前一个语句执行完毕后才会执行下一个语句。
异步(Asynchronous): 在异步代码执行中,某些任务会被推迟到稍后执行,而不是立即执行。异步代码通常涉及到需要等待的操作,比如网络请求、文件读取、定时器等。当遇到这些异步操作时,JavaScript会继续执行后续代码,而不是等待异步操作完成。当异步操作完成后,会触发相应的回调函数或执行绑定的事件处理程序。
示例9:
console.log("Start");
setTimeout(function() {
console.log("Async Task Done");
}, 2000);
console.log("End");
在上述代码中,setTimeout
函数设置了一个2秒的定时器,它会在2秒后执行回调函数,并输出"Async Task Done"。而在定时器等待的2秒内,代码会继续执行后续的console.log("End")
语句。
(1)js是单线程的,但是分同步异步
(2)微任务和宏任务皆为异步任务,它们都属于一个队列
(3)宏任务一般是:script、setTimeout、setInterval、postMessage
(4)微任务:Promise.then ES6
(5)先执行同步再执行异步,异步遇到微任务,先执行微任务,执行完后如果没有微任务,就执行下一个宏任务,如果有微任务,就按顺序一个一个执行微任务
同步>微任务>宏任务
宏任务代表的是一组异步任务,这些任务通常包含了用户交互、定时器事件、网络请求等。宏任务的执行是在当前执行栈的所有任务执行完毕后才进行。在执行过程中,如果有新的宏任务被加入,它会排在队列的末尾等待执行。
宏任务一般是:script、setTimeout、setInterval、postMessage
微任务代表的是一组异步任务,这些任务的执行在当前宏任务执行结束后、下一个宏任务开始之前进行。也就是说,微任务的执行优先级比宏任务高。如果在一个宏任务中产生了微任务,那么这些微任务会在当前宏任务执行结束后立即执行。
微任务:Promise.then
示例10:
//宏任务 放进队列
setTimeout(function(){
console.log(1);
});
//微任务
new Promise(function(resolve){
console.log(2);
resolve();
}).then(function(){
console.log(3);
}).then(function(){
console.log(4)
});
//同步代码
console.log(5);
// 2 5 3 4 1
这道题可能会产生误解,认为会先输出5,但是promise和promise.then执行代码的顺序是不一样的
遇到setTimout,异步宏任务,放入宏任务队列中
遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
Promise.then,异步微任务,将其放入微任务队列中
遇到同步任务console.log(5);输出5;主线程中同步任务执行完
从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空