简单的一点 JavaScript 理解:函数、对象、执行上下文、this
近期面一下前端,整理一下自己对一些知识点的理解。
每个人都知道(逃),JavaScript 有两类数据类型:基本 和 引用 数据类型。
基本类型数据保存在在栈内存中;引用类型数据保存在堆内存中,而它的变量是一个指向堆内存中实际对象的引用,存在栈中。所以只使用
=
赋值,它无法做到深层的拷贝。所以就有了深拷贝和浅拷贝的机制,区别在是否开拓一个新的栈,从而隔离拷贝出来的新对象。
对象、函数之类的数据就属于引用数据类型。
而原型链的机制和各种各样的设计,意味着 JS 当中对象和函数是不分家的。函数是 Function 对象的一个实例,可以作为参数、返回值,可以被赋值给变量,也可以被函数修改,返回一个新的函数(装饰器)…
作为存在面向对象特性的语言,this 在 JavaScript
当中也存在。this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。在函数执行过程中,this
一旦被确定了,就不可以再更改。
上下文、this、闭包
然而,this 的值不由对象决定,而由代码执行时调用它的上下文实时决定。。。
优先级的顺序是:(从高往低)
- new 绑定(这个函数是构造函数,创建实例的时候 this 指向实例对象。let obj = new Test())
- 显式绑定(func. apply()、bind()、call(),改变 func 的 this 的调用对象,参数就是改变后调用的对象。)
- 隐式绑定(函数还可以作为某个对象的方法调用,其中的 this 指向这个上级对象)(函数执行上下文)
- 默认绑定(仅非严格模式,绑定到全局对象,this 指向 window)(也就是 全局执行上下文,全局对象也就是 window 对象。)
而上面提到的执行上下文(全局 / 函数 / eval),是一个存在生命周期的概念,分为创建 - 执行 - 回收,
创建
为了确保它可以工作,首先要对三个要素进行创建:this 的绑定、词法环境、变量环境 这三个组件。
当函数被调用时,会创建一个新的执行上下文,并形成一个新的作用域链。这个作用域链包括了当前执行上下文的词法环境和外部环境引用,沿着作用域链,JavaScript 引擎可以找到变量和函数的定义。
正因此,代码执行的时候才调用上下文,正因为调用上下文的时候绑定,这个过程才变得非常抽象,可能是因为这个随处用 this 的方法太野史了(全贬义),默认绑定在严格模式下不可用。
而显然,代码的执行上下文是嵌套的,优先级遵循 LIFO 的结构,所以 执行上下文 使用执行栈来管理。
第一行代码执行,全局执行上下文,push
遇到函数,函数执行上下文,push…
执行
执行代码。源代码声明的位置无变量,分配 undefined。
回收
出栈,等待虚拟机回收。
一个例子
1 | function makeUser() { |
TypeError: Cannot read properties of undefined (reading ‘name’)?
这个 this 被带出来的时候指向 window。
1 | function makeUser() { |
设置 this
的规则不考虑对象定义。只有调用那一刻才重要。
换绑 —— 装饰器模式,apply,bind,call
所以,也可以把含有 this 的函数,通过装饰器(返回一个函数的函数,用于修改函数)的模式来修改。
apply, bind, call 就可以改变函数执行时的上下文,不过细节有点区别。
1 | func.apply(context, [args]); |
和作用域的关系
当然,要区分两个概念就是作用域和执行上下文。作用域是由代码的语法、词法决定的,是静态的。作用域决定了执行上下文中的变量和函数的可见性和访问规则:当代码执行时,JavaScript 引擎会根据作用域链在执行上下文中查找变量和函数的定义。作用域链是由当前执行上下文的词法环境和外部环境引用组成的。