事件循环
js 是一门单线程非阻塞的脚本语言。
js 事件的执行由执行栈和事件队列组成。
同步函数在执行过程中会加入到执行栈中执行,
异步函数在执行过程中会先在事件表中注册事件,等到异步函数得到执行结果后再把回调函数加入到事件队列中去。
执行过程是先执行执行栈中的事件,等到执行栈中的事件执行完毕之后再检查事件队列还有没有事件,
如果有事件则把事件加入到执行栈中执行。依次往复,所以称为事件循环。
异步事件还分为微任务和宏任务,微任务会进入事件队列的微任务队列,宏任务会进入事件队列的宏任务队列。
常见的微任务有:
new Promise().then(callback)、MutationObserve等(async, await 是Promise的语法糖,本质和Promise一样)
常见的宏任务有:
setTimeout、setInterval、script(整体代码)、I/O操作、UI交互事件等
new Promise在实例化的过程中执行的代码是同步的,then中执行的回调函数才是异步的。
因为整体代码也属于宏任务,所以最后的执行过程是先执行宏任务然后再执行微任务,依次循环。
js 参数传递问题
function fn(o, val) {
o.b = 1;
val = 1;
}
var obj = {b: 0};
var num = 0;
fn(obj, num);
console.log(obj, num);
输出结果为
{b: 1} 0
解析:
js传参包括基本数据类型传参和复杂数据类型传参,基本数据类型传参实际上是复制值传递,
函数对形参的处理不会影响到外面的实参,
而复杂数据类型传参是引用传递,即把数据的地址复制一份后传递给函数中,此时形参和实参指向同一地址的对象,
对于形参的修改会影响到实参。
变量提升和函数提升
js 有变量提升和函数提升,指的是用 var 声明变量或 function 函数名(){} 声明的,
会在 js 预编译阶段提升到顶端;
其中,函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被变量赋值后覆盖。
console.log(foo);
var foo = 1;
console.log(foo);
foo();
function foo() {
console.log('函数');
}
返回结果
[Function: foo]
1
报错
解析
上述代码等价于
function foo() {
console.log('函数');
}
var foo;
console.log(foo); // var foo 没有赋值,所以不会覆盖函数foo
foo = 1;
console.log(foo);
foo() // foo被赋值为1,覆盖了foo函数,所以foo不再是函数类型,所以报错
this、apply、call、bind
this的指向,始终坚持一个原理: this 永远指向最后调用它的那个对象
三个小案例:
案例1:
var name = "windowsName";
function a() {
var name = "Cherry";
console.log(this.name); // windowsName
console.log("inner:" + this); // inner:Window
}
a();
console.log("outer:" + this); // outer:Window
a()可以写成Window.a(),最后是全局对象Window调用的,所以this指向的是Window这个全局对象
案例2:
var name = "windowsName";
var a = {
name: "Cherry",
fn: function() {
console.log(this.name); // Cherry
}
}
a.fn();
因为最后是a对象调用的fn方法,所以最后输出的是a对象里面的name属性
案例3:
var name = "windowsName";
var a = {
name: "Cherry",
fn: function() {
console.log(this.name); // Cherry
}
}
window.a.fn();
虽然调用写的是window.a.fn(),但是最后一个调用fn方法的是a对象,所以返回的是a对象里面的name属性。
改变 this 指向的方法有:
1. 使用 ES6 的箭头函数
2. 在函数内部使用 _this = this
3. 使用 apply、call、bind
4. new 实例化一个对象
箭头函数改变this指向
箭头函数的 this 始终指向函数定义时的 this,而非执行时。
箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,
如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,
否则,在严格模式下 this 为 undefined。
案例:
var name = "windowsName";
var a = {
name: "Cherry",
func1: function () {
console.log(this.name)
},
func2: function() {
setTimeout(() => {
this.func1()
}, 100);
}
};
a.func2(); // Cherry
因为 箭头函数的this始终指向函数定义时的this,而非执行时。
在定义的时候箭头函数的this作用域链找到对象a,
所以箭头函数的this指向的就是对象a,
最后指向a.func2()时返回的就是a对象里面的 func1。
使用 _this 改变this指向
这个方式就是在定义的时候通过 _this变量保存定义时的this指向,这样在执行的时候使用_this就不会出现this指向执行时调用的对象。
案例:
var name = "windowsName";
var a = {
name: "Cherry",
func1: function() {
console.log(this.name)
},
func2: function() {
var _this = this;
setTimeout(function() {
_this.func1()
}, 100);
}
};
a.func2(); // Cherry
通过 _this变量保存的是定义时this的指向,这是的 this 指向的还是对象a,所以在运行过程中使用 _this,那么即使this的指向改变了但是_this仍然指向定义时的对象a,所以最后返回的是对象a中func1。
使用 apply、call、bind 改变this指向
call 和 apply 都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即指向的。
调用 call 和 apply 的对象,必须是一个函数 Function。
call 的写法
Function.call(obj, [pararm1[,param2[,...[,paramN]]]])
需要注意以下几点:
* 调用 call 的对象,必须是一个函数 Function。
* call 的第一个参数,是一个对象。Function 的调用者将会指向这个对象。如果不传,则默认为全局对象 window。
* 第二个参数开始,可以接收任意个参数。每个参数会映射到 Function 的相应位置 的参数上。但是如果将所有的参数作为数据传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
apply 的写法
Function.apply(obj[,argArray])
apply 的用法与 call 的用法是一样的。区别是第二个参数,call第二个参数可以传任意多个,而 apply 的第二个参数只能传入一个,而且这个参数必须是数据或者是类数组。
bind 的写法
Function.bind(thisArg[,arg1[,arg2[,...]]])
thisArg 意思是Function的this要指向的对象。
后面时多个参数,分别映射给Function对应的属性上。
bind和call、apply不同的地方在于,bind方法的返回值是函数,并且需要调用才会执行。而 apply 和 call 则是立即调用。
new 改变 this 的指向
案例:
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
var a = new myFunction("Li", "Cherry");
a.lastName;
new 的过程
var a = new myFunction("Li", "Cherry");
new myFunction {
var obj = {};
obj._proto_ = myFunction.prototype;
var result = myFunction.call(obj, "Li", "Cherry");
return typeof result === 'obj' ? result : obj;
}
先创建一个空对象。然后将创建的空对象的隐式原型指向其构造函数的显示原型。使用 call 改变 this 的指向。然后根据返回值的类型返回不同的结果。
通过查看 new 过程可以很清楚的知道 new 改变 this 的指向的原理就是 call。