JavaScript 核心原理解析
周爱民
《JavaScript 语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
32699 人已学习
新⼈⾸单¥59
登录后,你可以任选3讲全文学习
课程目录
已完结/共 28 讲
开篇词 (1讲)
JavaScript 核心原理解析
15
15
1.0x
00:00/00:00
登录|注册

12 | 1 in 1..constructor:这行代码的结果,既可能是true,也可能是false

在表达式[]中加上符号“+-*/”并确保结果可作为表达式求值
表达式[]的求值过程
JavaScript中对象属性存取结果总是不确定的
属性存取结果受到原型继承链的影响
存取器带来的不确定性
包装类使得值类型数据也可以具有与之对应的包装类的原型属性或方法
浮点数的字面量解析过程
标识符与字面量的处理机制
属性描述符的类型决定了属性的行为
constructor属性是从原型继承来的一个属性
JavaScript中的对象仍然是简单的、原始的、使用JavaScript 1.x时代的基础设计的原型继承
属性描述符管理属性规则
ECMAScript约定了属性的可列举性
属性的可见性
JavaScript提出了“对象闭包”与“函数闭包”两个概念
全局变量会被绑定为全局对象的属性
对象是零到多个属性的集合
对象创建自类
思考题
知识回顾
属性存取的不确定性
字面量与标识符
从原型中继承来的属性
属性访问与可见性
JavaScript 1.0~1.3中的对象
JavaScript 面向对象系统

该思维导图由 AI 生成,仅供参考

你好,我是周爱民。欢迎你回到我的专栏。
如果你听过上一讲,那么你应该知道,接下来我要与你聊的是 JavaScript 的面向对象系统
最早期的 JavaScript 只有一个非常弱的对象系统。我用过 JavaScript 1.0,甚至可能还是最早尝试用它在浏览器中写代码的一批程序员,我也寻找和收集过早期的 CEniv 和 ScriptEase,只为了探究它最早的语言特性与 JavaScript 之间的相似之处。
然而,不得不说的是,曾经的 JavaScript 在面向对象特性方面,在语法上更像 Java,而在实现上却是谁也不像。

JavaScript 1.0~1.3 中的对象

在 JavaScript 1.0 的时候,对象是不支持继承的。那时的 JavaScript 使用的是称为“类抄写”的技术来创建对象,就是“在一个函数中将this引用添加属性,并且使用new运算来创建对象实例”,例如:
function Car() {
this.name = "Car";
this.color = "Red";
}
var x = new Car();
关于类抄写以及与此相关的性质,我会在后续的内容中详细讲述。现在,你在这里需要留意的是:在“Car()”这个函数中,事实上该函数是以“类”的身份来声明了一系列的属性(Property)。正是因此,使用new Car()来创建的“类的实例”(也就是对象this)也就具有了这些属性。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

JavaScript 1.0~1.3时代的对象系统基于原型继承,对象是属性的集合,继承由原型链构成。全局环境和变量的设计成熟,闭包和对象都能实现变量环境。JavaScript中的对象是简单的、原始的,使用了基础设计的原型继承。在原型继承中,子类实例重写属性时,实际上是在子类实例的自有属性表中添加一个新项。属性描述符的类型决定了属性存取的行为。除了存取器带来的不确定性,属性存取结果还受到原型继承的影响。JavaScript中的对象属性存取结果总是不确定的,取决于属性的自有性和属性描述符的类型。文章还提出了两道思考题,引发读者深入思考。文章内容涉及JavaScript对象的基础知识和属性存取的不确定性,以及包装类等复杂概念。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JavaScript 核心原理解析》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(12)

  • 最新
  • 精选
  • Smallfly
    1. [] 的求值过程 一开始没明白题目的意思,看到留言区的提示才理解,题目考察的是 JS 的类型转换,[] 属于对象类型,对象类型到值类型的转换过程是什么样的? 对象转值类型的规范过程称为:ToPrimitive 分为三个步骤: 1. 判断对象是否实现 [Symbol.toPrimitive] 属性,如果实现调用它,并判断返回值是否为值类型,如果不是,执行下一步。 2. 如果转换类型为 string,依次尝试调用 toString() 和 valueOf() 方法,如果 toString() 存在,并正确返回值类型就不会执行 valueOf()。 3. 如果转换类型为 number/default,依次尝试调用 valueOf() 和 toString(),如果 valueOf() 存在,并正确返回值类型就不会执行 toString()。 数组默认没有实现 [Symbol.toPrimitive] 属性,因此需要考察 2、3 两步。 [] + ’‘ 表达式为 string 转换,会触发调用 toString() 方法,结果为空字符,等价于 ’’ + ‘’ 结果为 ‘’。 +[] 表达式是 number 转换,会先触发调用 valueOf() 方法,该方法返回的是空数组本身,它不属于值类型,因此会再尝试调用 toString() 方法,返回空字符,+‘’ 结果为 0;

    作者回复: 加分加分。呵呵~ ^^.

    2020-02-08
    32
  • 墨灵
    有个小问题,当一个函数使用了函数外部的变量时,这种情况就能称为“闭包”吗? ``` // 函数f只是使用了全局的变量x let x = 0; function f (y) { return x + y; } // z引用g函数内的匿名函数,而匿名函数使用了g的参数x,而造成g的函数作用域无法释放。 function g (x) { return (y) => x + y; } const z = g(0); ``` 在第一种情况,f函数调用之后就可以释放作用域,而第二种情况,无论z调用多少次,只要z不指向别一个值,函数g的作用域就不会释放。这就是我所理解的函数闭包,但对象闭包就是什么样子的?

    作者回复: 对象闭包是在非严格模式中才能用的,例如: ``` let f, obj = new Object; with (obj) f = function() {}; ``` 这种情况下,with语句为对象obj创建了一个块级作用域,这个作用域(以及它的链)就被作为一个对象闭包放在函数f的作用域链上了。 在ES6之后已经不再使用对象闭包这样的说法,统一用块级作用域和环境来描述这些语法效果了。在ES6之前,由于函数的作用域(的实例)被称为闭包,所以对象的with作用域(的实例)也就称为对象闭包。 最后,事实上JavaScript的全局global也是一个对象闭包。它总是在其它所有闭包(作用域链)的顶端。 再再再补充一下,闭包跟作用域其实是不完全相同的。作用域通常是语法所对应的块,是静态概念的,而闭包是运行期才使用的概念,函数被调用一次就有一个闭包出现,但函数自身其实只有一个作用域。——所以,看起来作用域像是“类”,而闭包像是“对象”,闭包是作用域的“实例”。 并且,确实的,在ECMAScript规范中,闭包就是一个作用域“实例化”的结果。——并且“实例化”是在“函数调用”时实时地创建和发生的,是动态的、运行期的概念。

    2020-03-20
    9
  • 青史成灰
    老师,关于这句话有个疑问:“这个包装的过程发生于函数调用运算“( )”的处理过程中,或者将“x.toString”作为整体来处理的过程中。也就是说,仅仅是“对象属性存取”这个行为本身,并不会触发一个普通“值类型数据”向它的包装类型转换” 只是前半句好理解,但是后半句“对象属性存取这个行为本身”,这个对象是发生包装转化后的对象吗?如果是,那怎么感觉就变成了“先有鸡还是先有蛋”的问题了。。。如果不是,那么原始类型不存在属性存取这一说法啊

    作者回复: 关键确实就在“对象属性存取这个行为本身”。 原始类型(中的值类型)的对象属性存取行为,例如“`true.toString`”会发生什么呢?它仍然是一次有效的属性存取,但它的结果(Result)并不会被立即求值。之前我们说过了,必须等到决定它是作为rhs/lhs之后,才能确定它是用来“求值”,还是只是“作为一个引用”,对不对? 那么,当得到操作`true.toString`的结果(Result)之后,在决定下一个可能的操作之前,它是一个什么状态呢?——这种情况下,它是作为一个“引用(规范类型)”来传递的。考虑到“引用(规范类型)”作为一个原始语言(例如C)中的结构/记录类型,那么它的ref.base域存放的,将是“值true”,而ref.name域中存放的,将是属性名“toString”。 所以你看,在这个阶段中,“包装(boxing)”这个行其实并没有发生。true还是true值,并没有“变成”Object(true),对不对? 所以说,确实存在一个“将对象属性存取这个行为(的结果)——作为一个整体”的阶段,这个存取行为并没有发生包装。但是,如果如下发生后一步的行为(也就是“作为整体来处理的过程中),那么,“包装”就会发生了。例如,GetValue(ref),那么就会先将ref.base中的值转换成对象;又例如,true.toString(),就会先将ref.true转换为对象然后作为this值传入toString()。 所以,“包装(亦即是‘转换为对象’)”这个行为,其实是发生在`GetValue()这个内部操作`或`()这个运算符`等等这样的运算过程中的。 所以回到最开始的,总之,“对象属性存取”这个行为本身,就是还没有触发“包装”。但它得到了包装要用的材料,亦即是ref.base和ref.name,不过还得需要“下一步”的具体操作,才能决定“包装是否会发生”。

    2020-01-12
    5
  • K4SHIFZ
    抱歉老师我杠一下,自动分号插入在规范11.9章:When, as the source text is parsed from left to right, a token (called the offending token) is encountered that is not allowed by any production of the grammar, then a semicolon is automatically inserted before the offending token if one or more of the following conditions is true: The offending token is separated from the previous token by at least one LineTerminator. The offending token is }. ... 它说是before the offending token,在}之前插入,所以应该变为{;}+{},而不是{};+{}。这么理解对吗?虽然分号插在哪,不影响引擎会首先作为语句执行第一个{}

    作者回复: 是这样的,我们通常讨论ASI的时候,是有两种语境的,一种是“尽量少写;号”,另一种是“尽量所有语句都写;号”。 ECMAScript在讨论ASI的时候,是直接认为第一种语境的。也就是说,ASI是“为了那些不想写分号的人准备的工具”。这种情况下,才会有了它的第一条规则(这个来自 Isaac Schlueter 的描述): > 在一个 \n 字符总是一个语句的结尾总是“自动加上 ;号” 但是我正好是“总是尽可能写分号派”的。呵呵,真的。因为我是从Pascal语言过来的,所以多数情况下我会为每个语句后面加一个;号。这带来了一种习惯,也就是“语句总是以";"号结束”。比如语句: ``` if (true) { }; // <- 事实上这个分号可以不写 ``` 如果你遵循语句以";"号结束的原则,那么你可以规避ASI的效果,因为任何情况下都有正确的";"号。但是另外的问题是,多数情况下我们在大括号后面是不用";"号的,因为大括号本身就是块语句,块语句最后的"}"本来也是语句结束符,所以一般我们会写成“{...}”而不是"{...};"。 因为是在这种语境讨论ASI,所以我才会说,可以将"{};"后面的这个分号写出来,从而看到: > {}+{} 被解析成了 > {};+{} 这个样子。OK,好吧,无论如何,我得承认,ASI是为了解决“尽量使用\n而不是使用;来结束语句”的问题的。所以尽管都是";"号的问题,但我上面讨论的并不是在ECMAScript所说的那个ASI,我这样归为ASI的问题并不正确。

    2020-03-28
    2
    2
  • 红白十万一只
    关于[]求值过程无非是隐式类型转换,隐式调用toString 来看看{}+{}这道题 可能有两种结果 1,"[Object Object] [Object Object]" 2,NAN 首先一种理解,代码块{},而不是对象 符合Firefox的结果 {}; +{} +{}.toString() +"[Object Object]" Number("[Object Object]") NAN 第二种把{}当场一个字面量 结果也就是"[Object Object][Object Object]" 符合谷歌结果 查了ES规范,{}什么时候是字面量,什么时候是代码块 1,{}前面有运算符号时,当成字面量 2,{}后面有;或隐式插入;时当场代码块 老师能更详细讲解一下{},什么时候是代码块,什么时候是字面量么

    作者回复: 这个涉及到“语句的语法规则”,也就是JS解析时处理“识别语句”的问题。 比较简单的说法是, 1. 某些情况下回车和上一语句的自然终结可以作为语句结束符。 2. 除了上一特例之外,分号和文末结束符(EOF)将被理解为语句结束符。 3. 任何情况下,从语句开始解析整个文本块。 然而比较麻烦的就是第1条规则。因为它意味着JavaScript的一个称为“自动分号插入(ASI)”规则生效,这就是有些人不赞同写行末分号的原因。——事实上是因为JavaScript读到那些特定位置的回车符或自然终结,然后自动插入了分号。 以{}为例,如果它正好在上一行的结尾之后(例1),或者是一段代码文本的最开始,那么它就被理解为语句(例2)。如下两例: ``` // 例1:类声明语句的最后一个`}`被理解为上一语句的结束。所以返回是一个空语句的值:undefined x = eval(`class foo {}{}`) // 例2:eval执行的代码块以语句开始解析。所以这里将有一个空语句和一个单值表达式语句,而返回值为1 x = eval(`{}1`) ```

    2020-03-01
    1
  • kittyE
    1. 我理解,[] 作为单值表达式,要GetValue(v),但为啥结果是 [],不太明白,ecma关于GetValue的描述,感觉好复杂。 2. []*[]/++[[]][+[]]-[+[]] 我随便写了一个 还真的能有值,不知道这样理解对不对,求老师解惑

    作者回复: 第2题你的理解是对的,不过表达式可以再简一些。^^. 关于第一个问题,思考方向不是GetValue,而是toPrimitive。还有,它的结果不是[],它的求值结果是0。

    2019-12-13
    1
  • Astrogladiator-埃蒂纳度斯
    试述表达式[]的求值过程。 对照http://www.ecma-international.org/ecma-262/5.1/#sec-9.1 http://www.ecma-international.org/ecma-262/5.1/#sec-8.12.8 step1: []不是一个原始类型,需要转化成原始类型求值 step2: 这个隐式转换是通过宿主对象中的[[DefaultValue]]方法来获取默认值 step3: 一般在没有指定preferredType的情况下,会隐式转换为number类型的默认值 step4: []默认值为0 可以这么理解?这个preferredType在什么设置? 在上述表达式中加上符号“+-*/”并确保结果可作为表达式求值。 这个是不是只要保证表达式中是对象或者number类型或者设置了preferredType的其他l类型(除了null, undefined, NaN)

    作者回复: 第1个问题, 这样解释是不对的。[[DefaultValue]]是用在那些值类型的包装对象上的,例如5和new Number(5)之间的关系。而preferredType是另外一个问题,涉及JavaScript对“预期转换目标类型”的管理,不同的运算之间还不同(但都与具体的运算操作有关),与当前这个问题却没有太大的关系。 第2个问题的意思,是如何使一个表达式里面只出现“+-*/”和"[]",并且表达式还可以通过语法检测并计算求值。

    2019-12-11
    1
  • 言川
    也就是说,“Number.prototype.construtctor”与“1..constructor”相同,且都指向 Number() 自身。 其中第一个 'constructor' 拼错了

    作者回复: 谢谢🙏已提交编辑老师处理

    2021-07-19
  • Elmer
    我觉得两题的本质都是再说对象如何转换为值类型。[]的求值过程在于[]所处的表达式环境需要number还是string,然后执行array.prototype上对应的方法转换。 题二中+-*/都是需要number,所以只要不出现0/0的情况即可。

    作者回复: 题二其实是没有标准答案的。不过多做一点提示,就是+/-其实也是一正值和负值运算符,不一定非得当成加减号来用。正值和负值运算符一样也会导致类型转换。

    2020-01-03
  • 墨灵
    An object is a collection of properties and has a single prototype object. The prototype may be the null value. 昨天查了一下,ECMAScript规范更新对object的描述了。
    2020-04-09
    5
收起评论
显示
设置
留言
12
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部