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