this/闭包/作用域
一、作用域
- 作用域可以理解为:变量的作用域(变量作用的范围)
- 在 js 中,对象和函数同样也是变量。
1. 作用域类型
- 局部作用域:函数作用域、块级作用域
- 全局作用域:
<script></script>
标签、js 文件
2. 变量、函数提升
- var 的声明有三步:创建,初始化,赋值。不管 var 声明在哪一行,预解析时它的前两步创建和初始化都会被提升到作用域的顶端。
- function 与 var 区别就是,function 声明的 创建,初始化,赋值三步,在预解析时都会被提升到作用域的顶端。
- let const 的声明在那行,这行到作用域的顶端就会形成临时死域。
- let 和 var 声明一样有三部:创建,初始化,赋值。不一样的是 let 声明只有创建被提升,所以会产生临时死域。
- const 和 let 只有一个区别,就是 const 声明只有两步:创建,初始化两步,没有赋值过程。
临时死域示例:
{
// 作用域顶端 x 的临时死域--start
console.log(x) // Uncaught ReferenceError: Cannot access 'x' before initialization
// x 的临时死域--end
let x = 1
}
开始的时候我简单的认为 let const 是没有变量提升的,直到我看到了下面这段代码和方应杭老师的这篇博文 我用了两个月的时间才理解 let
x = "global";
// function scope:
(function() {
x; // not "global"
var/let/… x;
}());
// block scope (not for `var`s):
{
x; // not "global"
let/const/… x;
}
代码解读
x = 'global'
// 函数作用域
;(function () {
console.log(x)
var x = 1
})()
- console.log(x) 打印出来的不是 global,而是 undefined
- 说明 var 声明的 x 已经被提升了,如果没有被提升应该会顺着作用域链向上查找到 x = “global”
x = 'global'
// 函数作用域
;(function () {
console.log(x)
let x = 1
})()
- console.log(x) 打印出来的不是 global,而是控制台报错
Uncaught ReferenceError: Cannot access 'x' before initialization
- 说明 let 声明的 x 已经被提升了,只是存在临时死域,如果没有被提升应该会顺着作用域链向上查找到 x = “global”
x = 'global'
// 块级作用域
{
console.log(x)
const x = 1
}
- const 和 let 都是控制台报错
Uncaught ReferenceError: Cannot access 'x' before initialization
- 说明 const 和 let 声明的 x 已经被提升了,只是存在临时死域,如果没有被提升应该会顺着作用域链向上查找到 x = “global”
3. 作用域链
- 全局作用域链:由下往上找, 往上找不到再往下
<script>
var aaa = 'aaa';
</script>
<script>
console.log('我是上一个<script>标签中的:' + aaa); // 我是上一个<script>标签中的:aaa
</script>
- 局部作用域链:由里往外
var a = 1
function fn1() {
console.log(a) // undefined
var a = 2
}
fn1()
console.log(a) // 1
/* --
第一步:预解析,找 var function 参数 放到仓库
a = undefined;
fn1 = function fn1() {
console.log(a);
var a = 2;
}
第二步:逐行执行代码
表达式:a = 1;
函数调用:fn1() 执行函数调用的时候又开始了一个函数的作用域
函数里面的第一步预解析 a = undefined;
函数里面的第二步逐行执行代码 console.log(a);
console.log(a) 打印出 undefined 说明了是先找了函数内的变量 a
-- */
var a = 1
function fn1() {
console.log(a) // 1
a = 2
}
fn1()
console.log(a) // 2
/* --
第一步:预解析,找var function 参数 放到仓库
a = undefined;
fn1 = function fn1() {
console.log(a);
a = 2;
}
第二步:逐行执行代码
表达式:a = 1;
函数调用:fn1();执行函数调用的时候又开始了一个函数的作用域
函数 fn1 里面没有 var function 所以不用预解析
直接开始执行console.log(a);
console.log(a) 的时候在 fn1 预解析的小仓库中找不 a,js解析器会顺着 作用域链 找到上一级的 a = 1
-- */
二、this
1.函数的直接调用
function fn() {
console.log(this) // Window
}
fn()
2. 隐式绑定
const obj = {
fn() {
console.log(this)
},
}
obj.fn()
3. 显示绑定
call apply bind
const obj1 = {
name: '我是obj1',
fn() {
console.log(this.name)
},
}
const obj2 = {
name: '我是obj2',
}
obj1.fn.call(obj2)
obj1.fn.apply(obj2)
obj1.fn.bind(obj2)()
// 打印结果都是 我是obj2
手写 call
- call 挂载在哪里:Function.prototype
- call 是什么:改变运行上下文,入参:新的 this + 原函数的参数
Function.prototype.call1 = function (newThis, ...params) {
const fn = this // 原函数,即调用了 call1 的函数
newThis.fn = fn // 改变运行上下文
newThis.fn(...params)
delete newThis.fn
}
手写 apply
Function.prototype.apply1 = function (newThis, params) {
const fn = this
newThis.fn = fn
newThis.fn(...params)
delete newThis.fn
}
手写 bind
- bind 挂载在哪里:Function.prototype
- bind 是什么: 改变运行上下文,入参:新的 this + 原函数的参数
- bind 需要什么:返回一个可执行函数(上下文被改变的原函数)
Function.prototype.bind1 = function (newThis, ...params) {
const fn = this
newThis.fn = fn
return function (...params2) {
newThis.fn(...params, ...params2)
delete newThis.fn
}
}
Function.prototype.bind2 = function (newThis, ...params) {
const fn = this
return function (...params2) {
fn.call(newThis, ...params, ...params2)
}
}
const obj1 = {
name: '我是obj1',
fn(a, b) {
console.log(this.name, a + b)
},
}
const obj2 = {
name: '我是obj2',
}
obj1.fn.apply1(obj2, [1, 2]) // 我是obj2 3
obj1.fn.call1(obj2, 1, 2) // 我是obj2 3
obj1.fn.bind1(obj2, 1, 2)() // 我是obj2 3
obj1.fn.bind2(obj2, 1)(2) // 我是obj2 3
4. new - this 指向 new 出来的的实例对象
class testClass {
constructor(name) {
this.name = name
}
fn() {
console.log(`我是${this.name}`)
}
}
const obj1 = new testClass('obj1')
const obj2 = new testClass('obj2')
obj1.fn() // 我是obj1
obj2.fn() // 我是obj2
类中异步方法,this 有区别吗?
class testClass {
constructor(name) {
this.name = name
}
fn() {
console.log(`我是${this.name}`)
setTimeout(function () {
console.log(this) // Window
}, 100)
}
}
const obj1 = new testClass('obj1')
obj1.fn()
三、闭包
一、什么是闭包
一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
上面这段话来自 MDN,有点抽象,可以先不管,大概知道就行。
解读一下就是:闭包是一个组合:一个函数 + 周围状态的引用
二、一句话两个表现
先记住一句话就行:闭包最常见的两个表现就是,函数作为参数被传递,函数作为返回值被返回。
表现一:函数作为参数被传递
function aFn(fn) {
const aaa = 100,
bbb = 200
fn()
}
const aaa = 0,
bbb = 0
function bFn() {
console.log(aaa, bbb) // 0 0
}
aFn(bFn)
表现二:函数作为返回值被返回
function cFn(fn) {
const ccc = 100,
ddd = 200
return function () {
console.log(ccc, ddd) // 100 200
}
}
const ccc = 0,
ddd = 0
const dFn = cFn()
dFn()
三、帮助理解的小提示
-
闭包实际上变量作用域应用的特殊情况。
-
闭包自由变量的查找,是在函数定义的地方向上级作用域查找,而不是在执行的地方向上级查找。
-
自由变量:当前作用域未定义的变量。
-
函数执行的上下文,不是本身的词法作用域。
function a() { const c = 'ccc' return function () { console.log(c) } } const b = a() b() // b 函数的执行上下文是 window,但是它的本身的词法环境在它定义的地方,即 a 函数 return 的 function