导读: 执行上下文、作用域、作用域链、闭包、this指向这几个方面之前的联系还是比较强的,在这里就统一的整理一下
执行上下文
作用域
作用域链
闭包
this指向
在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个 全局执行上下文 环境 ,在这个环境中,所有变量提升、函数声明提前都会先拿出来,有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。(这是在代码执行之前开始的工作)
在 JavaScript 的世界里,运行环境有三种,分别是:
1. 全局环境:代码首先进入的环境
2. 函数环境:函数被调用时执行的环境
3. eval函数:https://www.cnblogs.com/chaoguo1234/p/5384745.html(不常用)
注意! 执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。
ES6 之前 JS 没有块级作用域,只有全局作用域和函数作用域
作用域就是一个独立的地盘,让变量不会外泄、暴露出去,也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
函数作用域:顾名思义就是在这个函数体里边才能访问的变量;
var a = 100
function fn() {
var a = 200
console.log('fn', a) //200
}
console.log('global', a) //100
fn()
块级作用域:ES6新增,用let命令新增了块级作用域,外层作用域无法获取到内层作用域,非常安全明了。即使外层和内层都使用相同变量名,也都互不干扰;
if (true) {
let name = 'zhangsan'
}
console.log(name) // 报错,因为let定义的name是在if这个块级作用域
首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)
要得到a
变量,但是在当前的作用域中没有定义a
(可对比一下b
)。当前作用域没有定义的变量,这成为 自由变量 。自由变量如何得到 —— 向父级作用域寻找。
var a = 100
function fn() {
var b = 200
console.log(a)
console.log(b)
}
fn()
如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) //100 自由变量,顺作用域链向父作用域找
console.log(b) //200 自由变量,顺作用域链向父作用域找
console.log(c) //300 本作用域的变量
}
F2()
}
F1()
闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包
function A() {
let a = 1
window.B = function () {
console.log(a)
}
通常我们也喜欢这么写
return function (){
console.log(a)
}
a=null
}
A()
B() // 1
很多人对于闭包的解释可能是函数嵌套了函数,然后返回一个函数。其实这个解释是不完整的,就比如我上面这个例子就可以反驳这个观点。没有返回函数而是将函数挂载到window上。
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
缺点是造成内存泄露,解决办法是使用完变量之后将其设置为null。
经典面试题,循环中使用闭包解决
var
定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 1000)
}
首先因为 setTimeout
是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
解决方法:第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, 1000)
})(i)
}
在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
解决方法:第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
1000,
i
)
}
解决方法:第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 1000)
}
涉及面试题:如何正确判断 this?箭头函数的 this 是什么?
先搞明白一个很重要的概念 —— this
的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this
是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。
我们先来看几个函数调用的场景
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
foo: foo
}
obj.foo()
const c = new foo()
接下来我们一个个分析上面几个场景
foo
来说,不管 foo
函数被放在了什么地方,this
一定是 window
obj.foo()
来说,我们只需要记住,谁调用了函数,谁就是 this
,所以在这个场景下 foo
函数中的 this
就是 obj
对象new
的方式来说,this
被永远绑定在了 c
上面,不会被任何方式改变 this
由于js 中this的指向受函数运行环境的影响,指向经常改变,使得开发变得困难和模糊,call、apply、bind基本都能明了的绑定this的指向。
call 方法可以指定this 的指向(即函数执行时所在的的作用域),然后再指定的作用域中,执行函数
var name='globalName';
var age='globalAge';
var obj = {
name:'qfl',
age:'26'
};
var f = function () {
return this.name+' '+this.age
};
f(); // globalName globalAge
f.call(obj) // qfl 26
call 可以接受多个参数,第一个参数是this 指向的对象,之后的是函数回调所需的入参
func.call(thisValue, arg1, arg2, ...)
在全局调用的时候,第一个参数是下面几种情况时,指向的都是window全局 //globalName globalAge
apply 和call 作用类似,也是改变this 指向,然后调用该函数,唯一区别是apply 接收数组作为函数执行时的参数
func.apply(thisValue, [arg1, arg2, ...])
apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。
第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
bind 用于将函数体内的this绑定到某个对象,然后返回一个新函数
(call/apply则是可以指定this 的指向(即函数执行时所在的的作用域),然后再指定的作用域中,执行函数)
var d = new Date();
d.getTime() // 1481869925657
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
报错是因为,d.getTime 赋值给 print 后,getTime 内部的this 指向方式变化,已经不再指向date 对象实例了
var print = d.getTime.bind(d);
print() // 1481869925657
bind 接收的参数就是所要绑定的对象
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj);
newAdd(5,2) // 14
注意! 多个bind一起使用,生效的始终是第一个bind! (add.bind(obj).bind(window) ===> 指向的始终是obj)
区别:call和apply都是立即执行的,bind会返回一个函数,然后再执行函数才会生效
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()()) //window
首先箭头函数其实是没有 this
的,箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this
。在这个例子中,因为包裹箭头函数的第一个普通函数是 a
,所以此时的 this
是 window
。箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。另外对箭头函数使用 bind
这类函数是无效的。
以上就是 this
的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this
最终指向哪里。
首先,new
的方式优先级最高,接下来是 bind
这些函数,然后是 obj.foo()
这种调用方式,最后是 foo
这种调用方式,同时,箭头函数的 this
一旦被绑定,就不会再被任何方式所改变。