假设你走进了一座叫“JavaScript 村”的百年老宅。客厅是全家共享的公共区域,谁都能往里摆东西——这就像 JavaScript 的全局作用域。楼上楼下每个房间各有各的规矩,谁住进去谁做主——这是函数作用域。今年老宅重新装修,添置了一批“自动分类储物柜”,一趟跑腿就能按标签把所有杂物分门别类——这正是 ES2024 新增的数组分组特性。管家老王在这家干了三十年,哪怕退休搬去别处,也还记得老宅每间房钥匙的挂法——这就是闭包:函数记住了自己“出生时”望见的那个环境。最后,周家的家谱清楚写着:孙辈遇到难题先找爸爸,爸爸解决不了再找爷爷——这是原型链:对象找不到属性,就沿着家谱一路往上问。学 JavaScript 核心,本质上就是学会这座老宅的储物规矩、用人智慧与家族传承。

岗位关联

掌握 JavaScript 核心语法与继承机制,是搭建 AI 前端交互、浏览器端智能代理的必备地基;理解闭包与原型链,能让你真正读懂 React、Vue 以及大模型 Web 前端框架的源码逻辑。

目录
  1. 可视化演示
  2. 1. ES2024:老宅的新装修
  3. 2. 变量与作用域:房间的规矩
  4. 3. 函数新语法:新式菜单与自助取菜
  5. 4. 闭包原理:管家的记忆
  6. 5. 原型链机制:家谱的规矩
  7. 6. 继承与类:立契约传家业
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示JS四大核心概念:作用域链查找、闭包变量捕获、原型链继承、模块化封装模式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

JavaScript作用域链查找机制 Global→Function→Block三层作用域,let/const创建块级作用域 变量查找沿作用域链向上逐层搜索,找到即停未找到报ReferenceError scope_chain = global → function → block 作用域链决定了变量的可访问范围和查找顺序

1. ES2024:老宅的新装修

老宅今年迎来了一次大焕新。以前把杂物放进储物柜,你得一件件手动分类;现在新装的“智能分类柜”只要说一句“按用途归类”,它就自己把所有东西分进贴着标签的格子里。ES2024 的数组分组特性,就是这排智能储物柜——它让前端开发者省掉了大量手写循环 if 的琐碎工作。

在 ES2024 之前,要把一组学生的分数按“及格/不及格”分成两堆,你得先建两个空数组,再遍历、判断、push。现在 Object.groupBy 一次搞定:传入数组和一个“贴标签函数”,它返回一个崭新的对象,键是标签,值是那些被贴了同一标签的元素。与之搭配的 Promise.withResolvers 则像是把储物柜的“开锁钥匙”直接递到你手里——不用再绕到构造器回调里取 resolve 和 reject,尤其适合与 await 配合,让异步编排代码变得又短又直。

老宅的新柜子固然漂亮,但它也提醒我们:工具再新,也得知道物品该往哪个房间放。这就引出 JavaScript 最基础的“房间规矩”——作用域。

groupBy(arr,f)={k:{ x∈arr∣f(x)=k }}\text{groupBy}(\text{arr}, f) = \{ k : \{\, x \in \text{arr} \mid f(x) = k \,\} \}
这个公式是“智能分类柜”的说明书:把数组 arr 交给贴标签函数 f,返回一个对象,每个键 k 对应的是一组满足 f(x)=k 的元素。换句话说,groupBy 把“一堆物品”变成了“一个带标签的抽屉收纳盒”。
  1. 分组器签名groupBy 接收两个输入——待分类的数组 arr 和产生分类标签的键函数 f。
    groupBy(arr,  f)\text{groupBy}(\text{arr},\; f)
  2. 返回一个对象输出的整体是一个对象,每个键 k 对应一个子数组,像是每个抽屉都贴上便签。
    { k:arrk }\{\, k : \text{arr}_k \,\}
  3. 条件收集元素写进键 k 那一格的元素,恰好是所有满足 f(x)=k 的 x——抽屉里的每件物品都符合便签描述。
    arrk={ x∈arr∣f(x)=k }\text{arr}_k = \{\, x \in \text{arr} \mid f(x) = k \,\}

2. 变量与作用域:房间的规矩

老宅的客厅里摆着几件祖传大立柜,谁都能往里面塞东西,天长日久变得杂乱拥挤。这就是 var 声明:它的“生存范围”是整个函数,哪怕你写在 if 块里,它也像是被搬到了客厅,处处可见,容易撞车。新装修后的房间则换成嵌入式墙柜——let 和 const 只在当前房间(代码块)里存在,关上门,外面谁也碰不到。

let 和 const 还带来了一道隐形的“施工警戒线”:在变量声明语句执行之前,该变量处于“暂存死区”,你一旦提前访问,立刻报 ReferenceError。这就像房间在装修挂牌“施工中,禁止入内”,任何尝试闯空门的访问都会被保安拦下。const 则更进一步:储物柜的“位置”锁死了,不允许重新指向别的柜子,但柜子里放的物品本身可以被整理替换——所以 const 声明的对象,其属性依然可以修改。

理解了房间的可见范围,接下来要看房客们怎么优雅地“就餐点菜”——也就是函数的新式语法。它们让表达意图像写菜单一样简洁。

TDZ(x)={ t∣let/const x 已声明但未初始化,访问 x⇒ReferenceError }\text{TDZ}(x) = \{\, t \mid \texttt{let/const } x \text{ 已声明但未初始化,访问 } x \Rightarrow \text{ReferenceError} \,\}
用房间的话说:只要某个 let 变量所在的房间已经存在,但墙柜还没装好,任何想使用它的动作都会触发“施工警报”,必须等到装备完成的那一刻,警报才解除。
  1. 死区定义TDZ(x) 是一段“时间集合”,从变量 x 被声明的那一行开始,一直到它真正被初始化为止。
    TDZ(x)={ t∣⋯ }\text{TDZ}(x) = \{\, t \mid \cdots \,\}
  2. 声明未初始化条件描述了中间态——x 已经写入房间的登记册,但墙柜里还是空的。
    let/const x 已声明但未初始化\texttt{let/const } x \text{ 已声明但未初始化}
  3. 访问即报错在这个窗口期里任何读取行为都会触发 ReferenceError,相当于敲了未完成装修的门。
    访问 x⇒ReferenceError\text{访问 } x \Rightarrow \text{ReferenceError}

3. 函数新语法:新式菜单与自助取菜

老宅请客,原来要写冗长的“取菜流程”:先定义函数 function (a, b) { return a + b; },再手动拼接文案。现在有了新式菜单,箭头函数像一道快手菜:(a, b) => a + b 干净利落地完成了同样的加法。更妙的是,点菜的“服务员”不再有自己的身份(this),它继承了出生时所在房间的身份——这让回调代码里的 this 不再错乱。

解构赋值则是“自助取菜台”:面对一桌丰盛的数据对象,你直接写出想取的“菜名”,它们就被自动装进你的盘子。比如 const { name, age } = user 从对象里抽字段,const [x, y] = arr 从数组里按位取数,一行多值,代码可读性大大提升。默认参数就像餐厅的“家常做法”:顾客没点明口味(没传参),厨房自动用默认配方(默认值),比如 function greet(name = '客官')。

新菜单让点菜变得高效,但真正让老宅管理井井有条的,是一位退休管家留下的传奇——闭包。他记住的不是菜单,而是老宅每一间房间暗藏的钥匙。

f=(a,b)↦a+b⟺f(a,b)=a+bf = (a, b) \mapsto a + b \quad\Longleftrightarrow\quad f(a, b) = a + b
箭头的左边是“进料口”(参数),右边是“出菜口”(返回值)。左键右值,一目了然;它没有自己的 this,于是 this 永远指向“菜单所在的那张桌子”。
  1. 箭头左侧是参数(a, b) 表示这道函数菜需要两份食材。
    f=(a,b)↦⋯f = (a, b) \mapsto \cdots
  2. 箭头右侧是操作食材入锅后执行加法,出锅即结果。
    ⋯↦a+b\cdots \mapsto a + b
  3. 等价于传统写法箭头函数与 function f(a, b) { return a + b; } 在纯计算场景下行为一致。
    ⟺f(a,b)=a+b\Longleftrightarrow \quad f(a, b) = a + b

4. 闭包原理:管家的记忆

管家老王在老宅干了三十年,对每间房的锁型、每把钥匙的挂法了然于心。退休后他搬去了别处,但只要有人问他“东厢房第二格柜子的钥匙怎么配”,他依然能准确答出。为什么?因为他大脑里保存着老宅的环境地图。在 JavaScript 里,当内层函数引用了外层函数的变量,即使外层函数已经执行结束,内层函数依然“记得”那些变量所在的环境——这就是闭包。

闭包依赖两条底层规则。第一是词法作用域:函数能看见哪些变量,由它在代码里“写在哪”决定,而不是“在哪被调用”。第二是作用域链:内层函数查变量时,先在自己房间找,找不到就去外层房间,再找不到去更外层,直到全局客厅——这就是一条由内向外的可见通道。最经典的闭包用例是计数器:makeCounter 内部有一个 count,返回的函数每次执行 ++count,由于闭包引用的是变量本身,count 会在多次调用间持续累积,仿佛管家一直默默记着账。

管家的记忆让老宅的杂务得以延续,但老宅之所以能屹立百年,更靠一张写满名字的家谱——它决定了每一代人该朝谁开口要东西。这就是原型链。

closure(f)=( f,  lexicalEnv )\text{closure}(f) = (\, f,\; \text{lexicalEnv}\,)
闭包不是“函数复制了一份变量”,而是“函数带着一个指向出生环境的引用离家出走”。环境还在,引用还在,变量就永远活着。
  1. 闭包的两件套一个闭包由函数体 f 和它出生时所在的词法环境 lexicalEnv 组成。
    closure(f)=(f,  lexicalEnv)\text{closure}(f) = (f,\; \text{lexicalEnv})
  2. 环境提供变量f 中引用的自由变量,都从这份 lexicalEnv 里按名字查找。
    lookup(x)=lexicalEnv[x]\text{lookup}(x) = \text{lexicalEnv}[x]
  3. 查找逐层外扩若当前环境没有 x,就查它的外层环境,直到全局作用域,否则报错。
    lookup(x)→outer[x]→⋯→global[x]\text{lookup}(x) \to \text{outer}[x] \to \cdots \to \text{global}[x]

5. 原型链机制:家谱的规矩

周家的规矩很明确:小孙子想要零花钱,先找爸爸;爸爸摇头,再找爷爷;爷爷也没有,就只能作罢。JavaScript 的对象认“属性”也是同一套逻辑:当你写 obj.bark(),引擎先看 obj 自己有没有 bark,没有就找它的“家谱爸爸”,再没有就继续上溯,直到 Object.prototype,再往上就是 null——查无此物,返回 undefined。

这条家谱是怎么连起来的?每个由构造函数创建的对象,内部都有一个隐藏指针 __proto__,它指向构造函数的 prototype 对象。构造函数的 prototype 是一个普通的“传家手册”,里面放着所有实例共享的方法。所以 Dog.prototype 放 bark,所有 new Dog() 出来的实例都能顺着 __proto__ 找到它。Object.create 则是“指定认亲”的工具——你可以直接告诉引擎“这个新对象的家谱爸爸是那个对象”,从而自由编织原型关系。

家谱上的父父子子很清楚,但老宅真正变得秩序井然,是在新时代立下的一份正式“家规契约”——class。它把血缘关系写成了白纸黑字。

get(o,p)={o[p],p∈o(自有属性)get(o.proto,  p),p∉o\text{get}(o, p) = \begin{cases}o[p], & p \in o \text{(自有属性)}\\\text{get}(o.\text{proto},\; p), & p \notin o\end{cases}
这是一个递推公式:每次查找都先翻当前对象自己的口袋;翻不到,就沿着 __proto__ 把问题抛给家族长辈。直到某一层的口袋里摸到属性,或者问到了 null 大爷跟前,才停下来。
  1. 自有属性优先对象 o 身上直接挂着的属性 p,一次命中,不再外找。
    o[p],p∈oo[p], \quad p \in o
  2. 沿原型递推若 o 没有 p,接下来的问题是“o 的原型爸爸有没有 p”。
    get(o.proto,  p)\text{get}(o.\text{proto},\; p)
  3. 递归终止条件当 o.proto 为 null 时,整条链走到尽头,返回 undefined。
    get(null,  p)=undefined\text{get}(\text{null},\; p) = \text{undefined}
  4. 链的起点o.__proto__ 默认指向构造函数 Constructor.prototype。
    o.proto=Constructor.prototypeo.\text{proto} = \text{Constructor}.\text{prototype}

6. 继承与类:立契约传家业

家谱是口口相传的血缘记录,但到了现代,周家决定把继承关系写成正式契约:家规上写“周小二继承周一的生产作坊”,读起来一目了然。ES6 的 class 就是这份契约——它把原型链继承包装成更清晰易读的语法,同时增加 extends(认祖先)和 super(叫长辈)两个糖衣接口,让继承的意图像读条款一样明白。

在底层,class 依然走原型链:Child 类的 prototype 被设置为一个以 Parent.prototype 为原型的新对象,于是子类实例沿着 __proto__ 先找到子类手册,再找到父类手册。必须留意的是 constructor——它记录“这个手册属于哪个构造函数”。当你手写原型继承时,往往要补一句 Child.prototype.constructor = Child,否则这个“户口信息”会错乱。现代类语法里 constructor 由引擎自动维护,这也是它比手工原型更省心的原因之一。

从新装修的储物柜到管家记忆,再到家谱与契约,这座老宅的全部规矩已经尽收眼底。接下来,让我们看看这些规矩在 AI 应用的真实战场上如何大显身手。

Child.prototype=Object.create(Parent.prototype)\text{Child}.\text{prototype} = \text{Object.create}(\text{Parent}.\text{prototype})
这句话是“家庭契约”的公证条款:子类的手册(Child.prototype)不是简单复制父类手册,而是新造一本以父类手册为祖先的手册,保证孙辈查属性时,先看子类再看父类。
  1. 创建中间手册Object.create(...) 造出一个全新的空对象,它的 __proto__ 指向父类手册。
    Object.create(Parent.prototype)\text{Object.create}(\text{Parent}.\text{prototype})
  2. 挂到子类名下把这个新对象赋给 Child.prototype,子类实例就能沿链找到祖传方法。
    Child.prototype=⋯\text{Child}.\text{prototype} = \cdots
  3. 补上户口信息手动继承时要回写 constructor,让 Child.prototype.constructor 指向 Child 自己。
    Child.prototype.constructor=Child\text{Child}.\text{prototype}.\text{constructor} = \text{Child}

AI 应用场景

场景 1:AI 对话面板的状态管理

阶段:前端交互层 · 会话状态维持

解决的问题:传统方式把对话历史、令牌和请求计数放在全局变量里,多轮对话互相污染,难以隔离。用闭包封装会话状态后,每个对话实例拥有独立的记忆空间,互不干扰。

具体案例:某 AI 客服 Web 应用使用 createChatSession(apiKey) 返回一个带闭包的聊天函数,内部 messages 数组被 addMessage 和 getHistory 两个内层函数共同引用,外部无法篡改。每次调用 session.send('你好') 都会自动把历史上下文拼接进大模型请求。

场景 2:浏览器端 AI 推理 SDK 的类继承

阶段:模型加载与推理封装层

解决的问题:直接调用底层 WebGPU/WebAssembly 推理接口又长又容易出错。利用原型链与 class extends 封装出统一的 Pipeline 基类,不同模型(文本分类、图像分割)只写差异部分,公共的加载、缓存、日志逻辑全部由基类继承。

具体案例:某团队开发的浏览器内 AI 推理库定义 class BasePipeline { async load(model) {...} },再由 class TextPipeline extends BasePipeline {} 和 class ImagePipeline extends BasePipeline {} 分别实现 run 方法。用户代码只需 new ImagePipeline() 即可获得完整的加载与推理能力,原型的共享让打包体积缩小约 30%。

场景 3:AI 结果聚合中的数组分组

阶段:后处理与结果统计

解决的问题:批量调用大模型后,上千条返回结果需要按类别、置信度或耗时区间统计。手工 for 循环分组容易出错且代码冗长,改用 Object.groupBy 后统计逻辑变成一行表达式。

具体案例:一个智能客服质检系统对 5000 条对话打分,使用 Object.groupBy(results, r => r.score >= 0.8 ? 'pass' : r.score >= 0.6 ? 'review' : 'fail') 一键得到三档分布,再配合 Object.entries 生成报表喂给团队看板。

Python 演示

python41 行
class JSObject:    """模拟 JavaScript 对象的原型链查找"""     def __init__(self, proto=None):        self.props = {}        self.proto = proto     def set(self, key, value):        self.props[key] = value     def lookup(self, key, depth=0):        if key in self.props:            return self.props[key], depth        if self.proto is not None:            return self.proto.lookup(key, depth + 1)        return None, depth     def has_own(self, key):        return key in self.props  def main():    animal = JSObject(proto=None)    animal.set("speak", lambda: "Animal noise")     dog = JSObject(proto=animal)    dog.set("bark", lambda: "Woof!")     puppy = JSObject(proto=dog)    puppy.set("wag", lambda: "Wag tail")     for method in ("bark", "speak", "wag", "fly"):        func, depth = puppy.lookup(method)        if func is None:            print(f"查找 {method}: 未找到,链长 {depth}")        else:            print(f"查找 {method}: 第 {depth} 层原型命中 -> {func()}")  if __name__ == "__main__":    main()
目的:这个程序用三个 Python 对象模拟了 JavaScript 中“实例 → 原型 → 原型”的链式结构,并输出每个属性命中的原型层级,帮你直观理解 __proto__ 逐级查找的本质。
  1. class JSObject:定义一个模拟 JS 对象的最小容器:props 存放自有属性,proto 指向家谱爸爸。
  2. def lookup(self, key, depth=0):递归实现原型链查找,depth 记录命中的层级。
  3. if key in self.props:对应引擎的自有属性检查,命中即返回。
  4. return self.proto.lookup(key, depth + 1)自有属性没有时,把问题抛给原型对象,这就是 obj.__proto__ 的递推。
  5. return None, depth原型为 None 时返回未找到,对应原型链终点的 null。
  6. puppy.set("wag", lambda: "Wag tail")给最内层实例挂一个自有属性,验证“自查优先”规则。
  7. func, depth = puppy.lookup(method)从 puppy 触发查找,打印第几层命中,直观呈现整条查找路程。

常见误区

误区我觉得 `let` 就是 `var` 的加强版,主要是为了防止重复声明,其他用法都差不多。

正解let 与 var 最本质的区别在于作用域规则完全不同。var 是函数级作用域,会被提升到函数顶部;let 是块级作用域,只在 {} 内有效,且有“暂存死区”,声明前访问会直接报错。把 let 当成“严格 var”,会在循环、if 块等场景写出隐蔽的 bug。

为何常见:早期 JavaScript 教程大量使用 var,学习新语法时容易先入为主地把它看成老概念的升级包。

误区闭包不就是把外层变量当时的数值存一份在函数里嘛,以后函数就用那个副本。

正解闭包保存的是“变量本身的引用”,而不是值拷贝。著名的循环陷阱就是例证:for (var i = 0; i < 3; i++) 里注册的回调,最终全部输出 3,因为回调看到的是同一个 i 的最终值。要拿到每次的独立值,必须用 let 创建独立绑定,或再包一层立即执行函数。

为何常见:我们日常生活的“记笔记”习惯是复制信息,于是很自然地把闭包误解成快照,忽略了它实际持有的是活引用。

误区`__proto__` 和 `prototype` 不就是都表示原型吗?两个随便用都行。

正解它们是两个不同主体上的不同属性。prototype 挂在构造函数上,是一个“传家手册”对象;__proto__ 挂在每个普通对象上,是指向“自家家谱爸爸”的指针。实例的 __proto__ 恰好指向构造函数的 prototype,两者在逻辑上首尾相接,但绝不是同一种属性。

为何常见:两者经常指向同一块原型对象,平时打印出来看着差不多,容易让初学者误以为它们是一枚硬币的两面。

误区箭头函数就是更短的 `function` 写法,哪里都能替换,写起来还省事。

正解箭头函数没有自己的 this、arguments、super,它的 this 在定义时就固定为外层词法环境的 this。作为对象方法或需要动态 this 的场景,箭头函数会导致 this 指向错误。它还不可作为构造函数,new 调用会抛错。

为何常见:很多入门示例只展示箭头函数“短、简洁”的一面,埋没了它在 this 处理上的深层差异。

误区`class` 是 JavaScript 新发明的面向对象机制,和原来的原型链没关系了。

正解class 只是原型链的语法糖,底层依然是构造函数、prototype 和 __proto__ 组成的继承网络。extends 对应 Object.create(Parent.prototype),方法定义挂到 Child.prototype 上,实例查找属性还是沿原型链走。

为何常见:class 看起来像 Java/C# 的类,人们就误以为 JS 抛弃了原型体系,实际上它只是给原型链穿了一件更体面的西装。

面试题

Q1. 下列哪个声明关键字创建的变量只在当前代码块内有效?

单选题 · easy

  • A.var 声明拥有函数级作用域
  • B.let 声明只在块内有效← 正确答案
  • C.function 声明会整体提升
  • D.全局声明随处都可以使用

答案:B。var 是函数级作用域,function 声明提升到作用域顶部,全局声明自然处处可见。只有 let(以及 const)将变量的可见范围限定在最近的 {} 代码块内,这正是 ES 模块化编程推荐它的原因。

Q2. 用 const 声明一个对象后,可以修改它吗?

单选题 · easy

  • A.完全不能修改该对象
  • B.只能修改基本类型的值
  • C.可以修改对象内部属性← 正确答案
  • D.只能整体重新赋值一次

答案:C。const 锁死的是“指针绑定”——变量不能再指向别的对象,但对象内部的属性依然可增删改。就像墙柜位置固定了,里面的物品还能重新整理。A 过于绝对,B 与对象无关,D 中 const 连一次重新赋值都不允许。

Q3. 闭包可以让函数记住什么?

单选题 · easy

  • A.函数定义时的词法作用域← 正确答案
  • B.函数调用时的全局对象
  • C.函数的参数数量统计
  • D.浏览器地址栏完整路径

答案:A。闭包的核心是“函数 + 定义时的词法环境”,因此它能记住出生时可见的变量。B、C、D 都是运行时或无关信息,与闭包机制无关。记住定义环境,才是闭包能实现计数器、模块化的根本原因。

Q4. 下列哪个属性指向创建该对象的构造函数的原型?

单选题 · mid

  • A.proto 指向构造函数的原型← 正确答案
  • B.prototype 属性属于实例对象
  • C.constructor 指向实例本身
  • D.prototype.chain 是继承链

答案:A。普通对象的 __proto__ 指向构造函数的 prototype,这正是属性查找的起点。prototype 属于构造函数而非实例,constructor 指向构造函数本身,而不存在 prototype.chain 这种官方属性。

Q5. 下列哪个属于 ES2024 中引入的新特性?

单选题 · mid

  • A.Object.groupBy 数组分组方法← 正确答案
  • B.let 和 const 块级声明
  • C.箭头函数简洁写法
  • D.模板字符串嵌套使用

答案:A。Object.groupBy(及 Map.groupBy)是 ES2024 正式加入的分组能力。let/const、箭头函数、模板字符串都是 ES6 时代就有的旧特性,把它们误记为 2024,是对语言演进时间线不熟悉。

Q6. 执行下面代码,控制台会打印什么?

单选题 · mid

  • A.输出结果为 1
  • B.输出结果为 2← 正确答案
  • C.直接抛出语法错误
  • D.输出 undefined 值

答案:B。inner 形成闭包,它持有的是变量 a 的引用而非当时的快照。out() 返回后,闭包环境依然活着;随后 a 被改为 2,f() 读取到的就是最新值 2。这揭示了闭包“引用而非复制”的本质。

Q7. 哪些代码写法会形成闭包?

多选题 · mid

  • A.返回引用外层变量的内层函数← 正确答案
  • B.把内层函数传给异步回调使用← 正确答案
  • C.两个顶层函数互相调用不嵌套
  • D.函数体内同时使用两个全局变量

答案:AB。只要“函数 + 它引用的外层变量”发生了绑定,就形成闭包,无论这函数是作为返回值还是回调传出去。C 和 D 不存在嵌套结构,变量要么在全局要么无外层捕获,都不会形成闭包。

Q8. 关于 Promise.withResolvers 的特性,正确的有哪些?

多选题 · mid

  • A.直接返回 resolve 函数← 正确答案
  • B.直接返回 reject 函数← 正确答案
  • C.必须在构造器回调里声明
  • D.会延迟 Promise 的结算时机

答案:AB。withResolvers 一次返回 { promise, resolve, reject },让你无需在 new Promise(executor) 里取函数,尤其利于和 await 配合。C 恰好说反,它能脱离 executor;D 错误——它不改变任何异步结算时机。

Q9. 执行 p.bark() 能调用到 Dog.prototype.bark 的原因是什么?

单选题 · hard

  • A.Puppy.prototype 原型链查找命中← 正确答案
  • B.构造函数自身名称恰好匹配
  • C.全局作用域里存在同名函数
  • D.实例对象上有同名的自有成员

答案:A。p.__proto__ 是 Puppy.prototype,而它又被设置为以 Dog.prototype 为原型的新对象。p.bark 先查 Puppy.prototype,没有,再沿 __proto__ 到 Dog.prototype,命中。函数名、全局变量、自有属性都不是该调用能成功的原因。

Q10. 关于 ES6 class 继承机制,正确的说法有哪些?

多选题 · hard

  • A.class 在机制上仍是原型链继承← 正确答案
  • B.派生类构造器必须先调用 super()← 正确答案
  • C.静态方法可以被实例直接调用
  • D.类声明拥有变量提升的能力

答案:AB。class 是原型链语法糖,派生类 this 的初始化依赖 super() 先运行,所以 B 正确且是一种硬性约束。静态方法挂构造函数上,实例沿 __proto__ 访问不到;类声明处于临时死区,不提升,故 C、D 错误。