复盘js中的作用域与this

150 阅读9分钟

这篇文章主要收录了js作用域的深入理解和this。

1 提升原理(早期ECMA)

1.1 作用域

在es6之前,只有这几种作用域:

  • 全局作用域
  • 函数作用域
  • with语句形成的

为什么会出现变量\函数提升?

这与全局代码执行过程有关

1.2 全局代码执行过程

例如这段代码

var name1 = 20
var name2 = 30
var res = name1 + name2

1.代码被解析

v8引擎内部会帮助我们创建一个全局对象(GlobalObject->GO)

并且会将这些全局变量放到该全局对象中,还未进行赋值操作

var name1 = 20
var name2 = 30
var res = name1 + name2
​
var globalObject = {
    String: '类',
    Date: '类',
    setTimeout: "函数"
    window: this,
    name1: undefined,
    name2: undefined,
    res: undefined
}

2.运行代码

为了执行代码,v8引擎内部会有一个执行上下文execute context stack(函数调用栈);

而我们执行的是全局代码,为了全局代码能够正常执行,需要创建全局执行上下文(Global Execution context)(全局代码需要被执行时才会被创建);

全局执行上下文里面有个variable object(VO)指向的是GO;

开始执行代码;

作用域提升的原理就是这样:

name1、name2、res一开始被放进了全局对象GO,并且值都为undefined,此时代码还未执行(编译阶段);

然后执行代码,为他们分别赋值;

要是在执行代码之前前想获取他们的值,只会是undefined;

变量是这样提升的,那函数呢?

1.3 函数执行过程

var name = 'zsf'
foo(123)
function (num) {
    console.log(m)
    var m = 10
    var n = 20
}

同样,解析到函数时,会在调用栈里创建一个函数执行上下文(FEC),FEC包含2部分:

  • VO:指向AO
  • 执行代码

然后将num、m、n变量放到AO对象中,并赋值为undefined

执行代码后才会给num、m、n赋值

在此之前打印会是undefined

image-20220303151618962.png

1.4 作用域链

查找一个变量时,真实的查找路径是沿着作用域链找的

var name = 'zsf'
foo(123)
function (num) {
    console.log(m)
    var m = 10
    var n = 20
    console.log(name)//zsf
}

image-20220303153149569.png

其实AO有两部分组成:

  • AO本身
  • 父级作用域(这里是GO)

当在AO里面找不name时,会去GO里找

父级作用域在编译时已经确定了,与定义时位置有关系,而与调用时位置没有关系

函数里嵌套函数同理

var name = 'zsf'
foo(123)
function (num) {
    console.log(m)
    var m = 10
    var n = 20
    function bar() {
        console.log(name)
    }
    bar()//zsf
}

image-20220303153304815.png

函数执行完对应的FEC就会弹出栈

1.5 面试题

var n = 100
function () {
    n = 200
}
console.log(n)//200
function foo () {
    console.log(n)//undefined
    var n = 200
    console,log(n)//200
}
var n = 100
foo()
var a = 100
function () {
    console.log(a)//undefined
    return
    var a = 10
}
//return是执行阶段的,编译阶段依然会有a:undefined放VO中

2 this

2.1 为什么需要this?

没有this,平常写代码很不方便,拷贝一个对象时,很多地方可能都需要修改

2.2 this的指向

与函数定义时位置无关,与函数调用时位置有关

function foo() {
  console.log(this);
}
// 1.直接调用这函数
foo()//window对象

var obj = {
  name: 'zsf',
  fn: foo
}
// 2.创建一个对象,对象中的函数指向foo
obj.fn()//obj对象
// 3.apply调用
foo.apply('123')//String对象

2.3 绑定规则

默认绑定

独立函数调用,指向window

函数调用时没有调用主体

function foo() {
  console.log(this)
}

foo()//window对象
var obj = {
  name: 'zsf',
  fn: function foo() {
    console.log(this)
  }
}

var bar = obj.fn
bar()//window,依然没有调用主题

隐式绑定

v8引擎绑定的

函数通过某个对象进行调用的,this绑定的就是该对象

前提条件:

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
  • 正是通过这个引用间接的将this绑定到了这个对象上
var obj = {
  name: 'zsf',
  fn: function foo() {
    console.log(this)
  }
}
obj.fn()//obj对象
var obj1 = {
  name: 'zsf',
  fn: function foo() {
    console.log(this)
  }
}
var obj2 = {
  name: 'obj2',
  fn: obj1.fn
}

obj2.fn()

显式绑定

如果不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,怎么做?

每个函数对象都有这2个方法

  • call()
  • apply()
  • bind()
function foo() {
  console.log("被调用了", this);
}
// 直接调用和call()/apply()调用的区别在于this绑定不同
// 直接调用this指向window
foo()
// call()/apply()调用会指定this绑定对象
var obj = {
  name: 'obj'
}
foo.call(obj)//obj
foo.apply(obj)//obj
foo.apply("aaa")//aaa

直接调用和call()/apply()调用的区别在于this绑定不同

  • 直接调用this指向window
  • call()/apply()调用会指定this绑定对象

call和apply的区别

传参方式不同,call接收多个参数是以逗号分开,而apply会将多个参数放数组

function sum(num1, num2) {
  console.log(num1 + num2, this)
}

foo.call('call', 20, 30)
foo.apply('qpply', [20, 30])

bind的显示绑定

function foo() {
  console.log(this)
}
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')

等价于

function foo() {
  console.log(this)
}
// 隐式绑定和显式绑定冲突了,根据优先级,显式绑定
var newFoo = foo.bind('aaa')
newFoo()
newFoo()
newFoo()
newFoo()

bing绑定之后会生成一个新的函数返回

new绑定

js中的函数可以当做一个类的构造函数来使用,也就是使用new关键字

function Person(name, age) {
  this.name = name
  this.age = age
}

var p1 = new Person('zsf', 18)
console.log(p1)

通过一个new关键字调用函数时(构造器),这个时候this是在调用这个构造器时创建出来的,并且this = 创建出来的对象

2.4 规则之外

忽略显式绑定

function foo() {
  console.log(this)
}

foo.call(null)
foo.call(undefined)

call、apply、bind当传入参数为null或undefined时,自动将this绑定到window对象

间接函数引用

var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this)
  }
}

var obj2 = {
  name: 'obj2'
};

(obj2.bar = obj1.foo)()//this
(obj1.foo)()//obj1

这属于独立函数调用(无等号就是隐式绑定

箭头函数

基础语法

箭头函数不会绑定this、arguments属性

箭头函数不能作为构造函数来使用(不能和new关键字一起使用)

  • ()
  • =>
  • {}

简写:

  • 只有一个参数可以省()
  • 只有一行执行提可以省{} ,并且会将该行代码当结果返回
  • 执行体只有一行且返回的是一个对象,小括号()括起来
var nums = [1,2,3]
nums.forEach(item => {
    console.log(item)
})
var nums = [1,2,3]
nums.filter(item => item % 2 === 0)
var bar = () => ({name: 'zsf', age: 18})

规则

箭头函数不绑定this,而是根据外层作用域来决定this

var foo = () => {
  console.log(this)
}

foo()//window

var obj = {
  fn: foo
}
obj.fn()//window

foo.call('abc')//window

这样有什么应用呢?

在箭头函数出来之前

var obj = {
  data: [],
  getDate: function () {
    // 发送网络请求,将结果放上面的data属性中
    var _this = this
    setTimeout(function () {
      var result = ['abc', 'bbv', 'ccc']
      _this.data = result
    }, 2000)
  }
}
// 由于这里的隐式绑定,第5行的this绑定了obj对象。才有了,第8行的写法
obj.getDate()

箭头函数出来之后

var obj = {
  data: [],
  getDate: function () {
    // 发送网络请求,将结果放上面的data属性中
    setTimeout( () => {
      var result = ['abc', 'bbv', 'ccc']
      this.data = result
    }, 2000)
  }
}
obj.getDate()

箭头函数不绑定this,相当于没有this,会寻找上层作用域寻找this;

也就是在getData的作用域里找this,而obj.getData已经隐式绑定了getData里的this指向obj

2.5 一些函数的this分析

setTimeout

setTimeout(function () {
  console.log(this)//window
}, 1000)

setTimeout内部使用的独立函数调用,所以this默认绑定window对象

2.6 规则优先级

  • 默认最低
  • 显式高于隐式
  • new高于隐式
  • new高于显式
var obj = {
  name: 'obj',
  fn: function foo() {
    console.log(this)
  }
}

obj.fn.call('abc')//abc
function foo() {
  console.log(this)
}
var obj = {
  name: 'obj',
  fn: foo.bind('abc')
}

obj.fn()//abc
var obj = {
  name: 'obj',
  fn: function () {
    console.log(this)
  }
}

var p = new obj.fn()//fn函数对象
function foo() {
  console.log(this)
}
var bar = foo.bind('aa')
var p = new bar()//foo

由于call和apply都是主动调用函数,所以不能和new一起使用

3 实现apply、call、bind

3.1 call

补充:

展开运算符... (类似遍历)

var names = ['abc', 'abb', 'ccc']
function foo (n1, n2, n3) {
    
}
foo(...names)

自己实现

// 给所有函数加上一个自定义call
Function.prototype.sfcall = function (thisArg, ...args) {
  // 在这里可以执行调用者(函数)
  // 问题1:如何获取到是哪个函数调用了sfcall?
  var fn = this
  // 边界情况edge case1 对thisArg转成对象类型(防止传入非对象类型报错)
  
  // 边界情况edge case2 传入参数null/undefined
  thisArg = thisArg ? Object(thisArg) : window
  // 边界情况edge case2 调用者(函数)有一个或多个参数时
  // 如何执行调用者(函数)?
  thisArg.fn = fn
  // 边界情况edge case3 调用者有返回值
  var result = thisArg.fn(...args)
  delete thisArg.fn
  // 返回调用者的返回值
  return result
}

function foo(n1, n2) {
  console.log('foo执行了', this, n1, n2)
  console.log(n1 + n2);
}

foo.sfcall('sss', 1, 2)

3.2 apply

有时间再补

3.3 bind

有时间再补

4 内存管理

4.1 内存泄漏

存在该释放内存空间没有回收

4.2 垃圾回收

(Garbage Collection)GC

不再使用的对象,都称之为垃圾,需要被回收

那GC怎么知道哪些对象不再使用呢?

GC算法

  • 引用计数
  • 标记清除

引用计数

每个对象会有个count 只要有引用指向,就+1;当count=0时,就回收该对象

但是这种算法会存在循环引用的问题(两个相互引用),count永不为0

标记清除

设置一个(root object)根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用对象

image-20220303171753502.png

这种算法会解决循环引用问题

5 补充

5.1 with

with语句可以形成自己的作用域

var obj = {
  name: 'zsf'
}
with(obj) {
  console.log(name)
}

现在已经不推荐了

5.2 eval

全局函数

var str = 'console.log(123)'
eval(str)//123

不建议:

  • 可读性差
  • 可能会被修改内容,被攻击
  • 必须经过js解释器,不能js引擎优化,执行效率低

5.3 严格模式

es5提出

严格模式的限制:

  • 通过抛出错误来消除一些原有的静默(silent)错误
  • js引擎执行代码时可以进行更多的优化
  • 禁用ECMAScript未来版本可能会定义的语法

开启

  • js文件
  • 某个函数中

js文件

文件顶部写上 use strict

某个函数中

function () {
    'use strict'
    ...
}

严格模式常见限制

  • 意外创建全局变量
  • 不允许函数有相同参数名称
  • 静默错误
  • 不允许使用原先的8进制格式
  • 不允许with语句
  • eval函数不会向上引用变量
  • 独立函数(自执行)的this指向undefined
  • setTimeout的this指向依然是window