JS中的对象和构造函数、This指向以及绑定方法、闭包、同步异步、微任务宏任务

目录

一、对象和构造函数

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.微任务


一、对象和构造函数

1.对象是什么

面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

那么,“对象”(object)到底是什么?我们从两个层次来理解。

(1)对象是单个实物的抽象。

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2)对象是一个容器,封装了属性(property)和方法(method)。

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

2.构造函数

面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。

典型的面向对象编程语言(比如 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指向以及绑定方法

1.This指向

1.1.This的含义

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.2.使用场景

(1)全局环境

全局环境使用this,它指的就是顶层对象window 。也就是上面的示例2中的f()函数

(2)构造函数

构造函数中的this,指的是实例对象。也就是上面的示例1中的this.price

(3)对象的方法

如果对象的方法里面包含thisthis的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变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 引擎内部,objobj.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 提供了callapplybind这三个方法,来切换/固定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方法的参数,应该是一个对象。如果参数为空、nullundefined,则默认传入全局对象。

如果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,宏任务队列为空

猜你喜欢

转载自blog.csdn.net/CQ17743254852/article/details/131946281