工作后忙于各种需求,业余时间也更倾向于休息娱乐,学习提升的时间明显少了。最近看了知乎的一篇 写给初中级前端的高级进阶指南 ,决定按照其中的思路巩固基础、深入提高。

这里记录一下学习 冴羽的博客 - JavaScript 深入系列 的笔记。

# JavaScript 深入之从原型到原型链

从一个构造函数 First 和它的实例对象 first 说起

function First() {}
const first = new First();

实例的原型是其构造函数的 prototype 属性

Object.getPrototypeOf(first) === First.prototype
// true

__proto__ 并非标准,只是绝大多数 JS 引擎约定俗成的实现,相当于 Object.getPrototypeOf() 的一个 getter

first.__proto__ === First.prototype
// true

构造函数 prototype 属性的 constructor 属性,是构造函数本身

First.prototype.constructor === First
// true

实例会继承原型上的属性

first.constructor === First
// true
First.prototype.foo = 'foo'
First.prototype.bar = () => 'bar'
first.foo
// "foo"
first.bar()
// "bar"

修改实例中的同名属性,不会影响其原型

first.foo = 'foobar'
first.bar = () => 'foobar'
First.prototype.foo
// "foo"
First.prototype.bar()
// "bar"

作为 first 原型的 First.prototype ,其本身又是一个对象 (Object),继承了 Object.prototype 作为原型

Object.getPrototypeOf(First.prototype) === Object.prototype

这种一层层的原型继承关系,就称之为“原型链”。所以 first 实例的原型链就是 First.prototype -> Object.prototype -> null

first.__proto__ === First.prototype
// true
first.__proto__.__proto__ === Object.prototype
// true
first.__proto__.__proto__.__proto__ === null
// true

实例从原型“继承”来的属性,实际上是依次沿着原型链查找对应的属性。例如几乎所有对象都有的 toString 方法,就是从 Object.prototype 上继承来的

first.toString === Object.prototype.toString
// true

Object.prototype 本身没有原型,它的原型是 null

在 JS 的世界里, Object.prototype 有点类似“万物之源”的味道

Object.getPrototypeOf(Object.prototype) === null
// true
Object.prototype.__proto__ === null
// true

# 对于原型和原型链的扩展

Object.create(foo) 可以创建一个以 foo 为原型的对象,当然这里 foo 本身也以 Object.prototype 作为原型

const foo = {}
Object.create(foo).__proto__ === foo
// true
foo.__proto__ === Object.prototype
// true

Object.create(null) 可以创建一个原型为 null 的对象,它没有任何可用的属性和方法,甚至连 __proto__ 这个 getter 也没有

const noPrototypeObj = Object.create(null)
Object.getPrototypeOf(noPrototypeObj) === null
// true
noPrototypeObj.__proto__ === undefined
// true

Object.getOwnPropertyNames() 可以只获取当前对象的属性名,而不会获取到其原型上的属性名。

const parent = {
  a: 'a',
  b() {},
}
Object.getOwnPropertyNames(parent)
// ["a", "b"]
const child = Object.create(parent)
Object.getOwnPropertyNames(child)
// []

Object.keys()Object.getOwnPropertyNames() 的作用类似,区别在于 Object.keys() 不会获取到不可枚举 (enumerable: false) 的属性

Object.getOwnPropertyNames(Object.prototype)
// ["constructor", .....]
Object.keys(Object.prototype)
// []

从上例可以看出,万物之源 Object.prototype 这个对象的所有属性都是不可枚举的,使用 Object.getOwnPropertyDescriptors() 方法可以查看当前对象(不包含原型链)属性的 descriptor ,其中就包含了是否可枚举

Object.getOwnPropertyDescriptors(Object.prototype)

in 关键字则会查找属性名是否存在与当前对象及其原型链上,包含 不可枚举的属性

'a' in child
// true
'toString' in child
// true

for .. in 循环则会遍历当前对象及其原型链上的所有属性,但 不包含 不可枚举的属性

for (let key in child) {
  console.log(key)
}
// a
// b

还有一个有意思的东西是 Function.prototype ,它是所有函数的原型

function foobar () {}
foobar.__proto__ === Function.prototype
// true

Function.prototype 中定义了函数的默认行为,包括函数是如何被调用的这些底层实现。其本身也是一个函数,可以被调用

typeof Function.prototype
// "function"
Function.prototype()
// undefined

我们刚才提到, Function.prototype 是“所有函数”的原型,其实是不准确的。因为它自己就是一个函数,但它并不是它自己的原型

Function.prototype.__proto__ === Function.prototype
// false
Function.prototype.__proto__ === Object.prototype
// true

在浏览器控制台打印 Function.prototype 会出现 [native code] ,说明它是通过原生代码而不是 JS 本身实现的,应该是在原生代码实现时,“篡改”了它的原型吧 (深入了解的话应该需要看 V8 的实现了)

Function.prototype
// ƒ () { [native code] }

# JavaScript 深入之词法作用域和动态作用域

JS 作用域类别:

  • 全局作用域 Global Scope
  • 函数作用域 Function Scope
  • 块级作用域 Block Scope (ES6 引入)

JS 使用的是词法作用域,即静态作用域,意思是作用域在静态词法分析的阶段就已经确定了,而并非在运行时动态决定。

拿函数作用域来说,函数的作用域在函数定义的时候就决定了,而不是执行的时候再决定。

实际上,函数定义时确定的只是作用域链 (Scope chain) 的查找顺序,在执行时才会真正在运行时的作用域链中查找变量。

# JavaScript 深入之执行上下文栈

执行上下文相关的内容,可以配合 可视化工具 直观感受

JS 的执行上下文,是存在一个栈中的。

开始执行 JS 时,会创建一个全局上下文在栈底,调用函数时创建新的上下文并压栈,函数返回时出栈。

# 执行上下文

JS 执行上下文类别:

  • 全局执行上下文 Global Execution Context
  • 函数执行上下文 Function Execution Context
  • Eval 执行上下文 Eval Execution Context

执行上下文会经历两个阶段:

  • 创建阶段 Creation Phase
    • 创建词法环境 Lexical Environment
    • 创建变量环境 Variable Environment
  • 执行阶段 Execution Phase
    • 代码执行,进行变量赋值、上下文创建等

Lexical Environment 由三部分组成:

  • 变量对象 Variable Object / 环境记录 Environment Record ,用于存储当前 Context 中的变量和函数
  • 作用域链 Scope Chain ,即对外层上下文的引用,可能为 null
  • "this" binding ,即对 "this" 关键字的绑定关系

Variable Environment 也是一个 Lexical Environment ,区别在于 Variable Environment 的变量对象存储的变量和函数是由 var 声明的,而 Lexical Environment 的变量对象存储的变量和函数是由 letconst 声明的。

理解了 Lexical Environment 的三个部分,也就理解了执行上下文的核心要素了。

后面三篇分别是对这三部分内容的深入。

# JavaScript 深入之变量对象

TODO