js语法细节
箭头函数中的this
它们没有 this。在箭头函数内部访问到的 this 都是从外部获取的。
可选链
- 通常我们这样写
let user = {}; // user 没有 address |
依次对整条路径上的属性使用与运算进行判断,以确保所有节点是存在的(如果不存在,则停止计算),但是写起来很麻烦。
- 通过可选链可以这样
let user = null; |
不要过度使用可选链
我们应该只将 ?. 使用在一些东西可以不存在的地方。
例如,如果根据我们的代码逻辑,user 对象必须存在,但 address 是可选的,那么 user.address?.street 会更好。
所以,如果 user 恰巧因为失误变为 undefined,我们会看到一个编程错误并修复它。否则,代码中的错误在不恰当的地方被消除了,这会导致调试更加困难。
|
这是因为我们需要变量 id 的值作为键,而不是字符串 “id”。
Symbol 属性不参与 for..in 循环。
Object.assign 会同时复制字符串和 symbol 属性
参考:symbol
对象原始值的转换
如果没有 Symbol.toPrimitive,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:
- 对于 “string” hint,toString -> valueOf。
- 其他情况,valueOf -> toString。
默认情况下,普通对象具有 toString 和 valueOf 方法:
- toString 方法返回一个字符串 “[object Object]”。
- valueOf 方法返回对象自身。
- 如果没有 Symbol.toPrimitive 和 valueOf,toString 将处理所有原始转换。
总结
对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。
这里有三种类型(hint):
- “string”(对于 alert 和其他需要字符串的操作)
- “number”(对于数学运算)
- “default”(少数运算符)
规范明确描述了哪个运算符使用哪个 hint。很少有运算符“不知道期望什么”并使用 “default” hint。通常对于内建对象,”default” hint 的处理方式与 “number” 相同,因此在实践中,最后两个 hint 常常合并在一起。
转换算法是:
1.调用 objSymbol.toPrimitive 如果这个方法存在,
2.否则,如果 hint 是 “string”
- 尝试 obj.toString() 和 obj.valueOf(),无论哪个存在。
3.否则,如果 hint 是 “number” 或者 “default”
- 尝试 obj.valueOf() 和 obj.toString(),无论哪个存在。
在实践中,为了便于进行日志记录或调试,对于所有能够返回一种“可读性好”的对象的表达形式的转换,只实现以 obj.toString() 作为全能转换的方法就够了。
参考 Symbol.toPrimitive
关于数组的length
当我们修改数组的时候,length 属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。
例如,一个数组只有一个元素,但是这个元素的索引值很大,那么这个数组的 length 也会很大:
let fruits = []; |
要知道的是我们通常不会这样使用数组。
length 属性的另一个有意思的点是它是可写的。
如果我们手动增加它,则不会发生任何有趣的事儿。但是如果我们减少它,数组就会被截断。该过程是不可逆的,下面是例子:
let arr = [1, 2, 3, 4, 5]; |
所以,清空数组最简单的方法就是:arr.length = 0;
关于JSON的转换
JSON 是语言无关的纯数据规范,因此一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过。
即:
- 函数属性(方法)。
- Symbol 类型的属性。
- 存储 undefined 的属性。
js中函数就是对象
被赋值给函数的属性,比如 sayHi.counter = 0,不会 在函数内定义一个局部变量 counter。换句话说,属性 counter 和变量 let counter 是毫不相关的两个东西。
我们可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然。它们之间是平行的。
关于this和箭头函数
箭头函数
- 没有 this
- 没有 arguments
- 不能使用 new 进行调用
- 它们也没有 super
所以箭头函数里的 this
的查找与常规变量的搜索方式完全相同:在外部词法环境中查找。
关于__proto__和prototype
初学者常犯一个普遍的错误,就是不知道
__proto__
和 [[Prototype]] 的区别。
请注意,__proto__
与内部的 [[Prototype]] 不一样。__proto__
是 [[Prototype]] 的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。
根据规范,```__proto__``` 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。 |
此外,我们还明确了 __proto__
是 [[Prototype]] 的 getter/setter,就像其他方法一样,它位于 Object.prototype。
我们可以通过 Object.create(null) 来创建没有原型的对象。这样的对象被用作 “pure dictionaries”,对于它们而言,使用 “__proto__
“ 作为键是没有问题的。
其他方法:
- Object.keys(obj) / Object.values(obj) / Object.entries(obj) —— 返回一个可枚举的由自身的字符串属性名/值/键值对组成的数组。
- Object.getOwnPropertySymbols(obj) —— 返回一个由自身所有的 symbol 类型的键组成的数组。
- Object.getOwnPropertyNames(obj) —— 返回一个由自身所有的字符串键组成的数组。
- Reflect.ownKeys(obj) —— 返回一个由自身所有键组成的数组。
- obj.hasOwnProperty(key):如果 obj 拥有名为 key 的自身的属性(非继承而来的),则返回 true。
所有返回对象属性的方法(如Object.keys 及其他)—— 都返回“自身”的属性。如果我们想继承它们,我们可以使用 for…in。
关于类继承
1.想要扩展一个类:class Child extends Parent:
- 这意味着 Child.prototype.proto 将是 Parent.prototype,所以方法会被继承。
2.重写一个 constructor:
- 在使用 this 之前,我们必须在 Child 的 constructor 中将父 constructor 调用为 super()。
3.重写一个方法:
- 我们可以在一个 Child 方法中使用 super.method() 来调用 Parent 方法。
4.内部:
- 方法在内部的 [[HomeObject]] 属性中记住了它们的类/对象。这就是 super 如何解析父方法的。
- 因此,将一个带有 super 的方法从一个对象复制到另一个对象是不安全的。
补充:
箭头函数没有自己的 this 或 super,所以它们能融入到就近的上下文中,像透明似的。
类检查”instanceof”
用于 | 返回值 | |
---|---|---|
type | 原始数据类型 | string |
{}.toString.call | 原始数据类型,内建对象,包含Symbol.toStringTag属性的对象 | string |
instanceof | 对象 | true/false |
如表所示:{}.toString.call (Object.prototype.toString) 可以检查对象的类型并返回字符串,而不是像toString仅仅返回 [Object,Object]
let s = Object.prototype.toString; |
模块的导入和导出
- 在声明一个 class/function/… 之前:
- export [default] class/function/variable …
- 独立的导出:
- export {x [as y], …}.
- 重新导出:
- export {x [as y], …} from “module”
- export * from “module”(不会重新导出默认的导出)。
- export {default [as y]} from “module”(重新导出默认的导出)。
导入:
- 模块中命名的导入:
- import {x [as y], …} from “module”
- 默认的导入:
- import x from “module”
- import {default as x} from “module”
- 所有:
- import * as obj from “module”
- 导入模块(它的代码,并运行),但不要将其赋值给变量:
- import “module”
我们把 import/export 语句放在脚本的顶部或底部,都没关系。
处理程序选项 “passive”
addEventListener 的可选项 passive: true 向浏览器发出信号,表明处理程序将不会调用 preventDefault()。
为什么需要这样做?
移动设备上会发生一些事件,例如 touchmove(当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的 preventDefault() 来阻止滚动。
因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用 preventDefault,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和“抖动”。
passive: true 选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件。
对于某些浏览器(Firefox,Chrome),默认情况下,touchstart 和 touchmove 事件的 passive 为 true。
async和defer
顺序 | DOMContentLoaded | |
---|---|---|
async | 加载优先顺序。脚本在文档中的顺序不重要 —— 先加载完成的先执行 | 不相关。可能在文档加载完成前加载并执行完毕。如果脚本很小或者来自于缓存,同时文档足够长,就会发生这种情况。 |
defer | 文档顺序(它们在文档中的顺序) | 在文档加载和解析完成之后(如果需要,则会等待),即在 DOMContentLoaded 之前执行。 |