作用域与作用域链
作用域链
- 保证对执行环境有权访问的所有变量和函数的有序访问。
- 案例分析
/**
* 在函数内部可以一次按顺序访问在test3中可以访问test1、test2中所有的变量
* 无法在test1中访问到,test2、test3中的变量
* 沿着作用域链一级一级的搜索,然后逐级地向上找,直至找到该变量为止,如果找不到,会报错
*/
let a = "a";
function test1() {
let b = "b";
test2()
function test2() {
let b = "b1";
// 打印:a b1 b会逐级往下找到最近的
console.log(a, b)
function test3() {
let d = "d";
console.log(a, b, d)
}
test3()
}
}
test1()
- 观察上述案例我们会发现,无论是在函数前面定义,使用函数,还是在函数定义后面使用函数,都是可以正常使用过的,为什么呢?
- 其实是方便开发者,不需要关注何时定义,可以直接调用。
- 也为目前打包工具tree-shaking埋下了伏笔。如果有未调用的,就可以通过tree-shaking将其去除。
作用域
- 块级作用域
- 使用{}扩起来的区域叫做块级作用域,if语句和for语句里面的{}。
- 所有用let和const声明的变量符合块作用域。
- 在块内使用let声明的变量,只会在当前的块内有效。
- 案例分析
// 通过 var 变量定义会提前声明定义。并不会产生块级作用域效果
if (true) {
var b = 2;
}
// 打印 2
console.log(b)
// 通过 let、const 变量定义只在当前块内生效
if (true) {
let c = 2;
console.log(c)
}
// 报错:Uncaught ReferenceError: c is not defined
console.log(c)
- 函数作用域
- 局部作用域,只和函数有关系,和其他外部无关。他也为现在的es module打下基础
- 在函数中声明var变量也是局部的,不会像块级一样辐射到外面。
- 案例分析
// 通过 var、let、const 变量定义都只在函数里面起作用
function test() {
var b = 2;
let c = 3;
const d = 4;
}
// 报错:Uncaught ReferenceError: b、c、d is not defined
console.log(b, c, d)
this指向
- this指向是动态执行的,简单来说就是,谁调用this指向谁
- 案例分析
const obj = {
text: 1,
fn: function () {
return this.text
}
}
/**
* 我们看到目前是obj调用的方法,那他的指向显然是obj所以打印为1
* 打印为:1。
*/
console.log(obj.fn())
const fn = obj.fn
/**
* 我们看到目前看到虽然是赋值,但是调用时他是直接调用的,赋值并不会改变指向。
* this也是动态,所以一定要等到调用功能的时候再看。
* 直接调用,我们会发现this指向的是window,里面没有text所以打印undefined
* 打印为:undefined。
*/
console.log(fn());
- 案例分析
const obj1 = {
text: 2,
fn: function () {
return this.text
}
}
const obj = {
text: 1,
fn: function () {
return obj1.fn()
}
}
/**
* 中间过程其实可以忽略,我们直接看谁调用,发现是obj1调用fn,他的指向很显然就是obj1。
* 打印为:2。
*/
console.log(obj.fn())
const obj1 = {
text: 2,
fn: function () {
return this.text
}
}
const obj = {
text: 1,
fn: function () {
const fn = obj1.fn
return fn()
}
}
/**
* 原理同上,你看我们虽然赋值了,但是调用的并不是obj1,而是直接调用,所以他的this指向的是window。
* 打印为:undifined。
*/
console.log(obj.fn())
综合上面三个案例来看,其实无论前置如何,最后都要看是谁来调用。才能决定this是谁。
- 如何更改this指向
- call、apply、bind
- 案例分析
const obj1 = {
text: 2,
}
const obj = {
text: 1,
fn: function (a) {
return this.text + a
}
}
// 打印: 5
console.log(obj.fn.call(obj1, 3), "call")
console.log(obj.fn.apply(obj1, [3]), "apply")
console.log(obj.fn.bind(obj1)(3), "bind")
console.log(obj.fn.bind(obj1, 3)(), "bind")
- 三种方式其实类似,但是也是有区别的
- apply传入函数的入参是数组的形式,call则是和函数相同
- bind是生成一个新的函数,参数既可以传bind,也可以传入新生成的函数中
- 手写bind
- 注意事项:new 的情况要考虑
/**
* bind绑定到Function.prototype中
* 考虑是否是new的情况
* 实现myApply
*/
Function.prototype.myBind = function (context, ...bindArgs) {
const _this = this;
const fBound = function (...args) {
// 如果时候new 返回this
thisArg = this instanceof fBound ? this : context;
return _this.myApply(thisArg, [...bindArgs, ...args])
}
fBound.prototype = Object.create(this.prototype);
return fBound
}
/**
* 找到执行函数 this
* 传入函数,入参
* 删除挂载函数
*/
Function.prototype.myApply = function (context, args = []) {
// 边界判断
if (typeof this !== 'function') {
throw new TypeError('Error is not a function')
}
const symbol = Symbol('fn');
context = context || window;
context[symbol] = this;
const result = context[symbol](...args);
delete context[symbol];
return result
}
function testFc(params) {
console.log(this, "xx", params)
}
testFc.myBind({ a: 2 }, 3)()