Giter Site home page Giter Site logo

es-blog's People

Contributors

shawlocke avatar

es-blog's Issues

Chapter 2. Reference record

Chapter 2. Reference record

The reference record type is used to explain the behaviour of such operators as typeof, the assignment operators and other language features. E.g. the left-hand operand of an assignment is expected to produce a reference record.

Reference record 是一个抽象概念,可以将其理解为一个 plain object.

Spec 用 specification type, 比如 reference record. 我们编写的 JS 代码用 language type,比如 Boolean, Number, String 等。

Reference record 用于 2 个地方:

  • with an identifier.

    如何确定 identifier 的 reference record 的[[Base]]请参考《Chapter 4 中的 identifier resolution》

  • with a property accessor (the dot notation or the bracket notation).

    会用到 spec 中的 EvaluatePropertyAccessWithIdentifierKey 或 EvaluatePropertyAccessWithExpressionKey 抽象运算

Reference record 的属性有:

  • [[Base]]:
    • any ECMAScript language value except undefined or null. (见示例 2 和 3)
    • an environment record. (见示例 1)
    • unresolvable. (见示例 4)

      对 unresolvable 的 reference record 应用 GetValue 抽象运算会导致 ReferenceError

  • [[ReferencedName]]: the name of binding.
  • [[Strict]]: true if in strict mode code, false otherwise.
// 示例 1
// foo的reference record:
// { [[Base]]: globalEnvRec, [[ReferencedName]]: 'foo', [[Strict]]: false }
const foo = 10;

// 示例 2
const bar = { x: 20 };
// 整个bar.x的解析过程如下:
// bar的reference record:
// { [[Base]]: globalEnvRec, [[ReferencedName]]: 'bar', [[Strict]]: false }
// 然后应用 GetValue ,得到一个对象
// bar.x的reference record:
// { [[Base]]: bar指向的对象(上面的对象), [[ReferencedName]]: 'x', [[Strict]]: false }
bar.x;

// 示例 3
const baz = null;
// 无法成功生成baz.toString的Reference record: EvaluatePropertyAccessWithIdentifierKey
// 会调用RequireObjectCoercible, `undefined`或`null`会导致TypeError
baz.toString; // TypeError: Cannot read properties of null

// 示例 4
// notFound的reference record:
// { [[Base]]: unresolvable, [[ReferencedName]]: 'notFound', [[Strict]]: false }
notFound; // ReferenceError: notFound is not defined (应用 GetValue 时导致的ReferenceError)

Chapter 1. Types, type conversion, operators

如错误或者不严谨的地方,请斧正,感谢。


Chapter 1. Types, type conversion, operators

In JavaScript, variables don't have types, but values do.

Types

A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, String, Symbol, and BigInt.
BigInt 用得很少,就没列出来

  • Undefined: undefined
  • Null: null
  • Boolean: true, false
  • Number
  • String
  • Symbol
  • Object
    • Object: {}
    • Array: []
    • Function: function foo() {}
    • Set: new Set()
    • Map: new Map()
    • Error: new Error('msg')
    • Date: new Date()
    • JSON: JSON.stringify
    • Math: JSON.floor
    • RegExp: /^foo$/gmi, g: global, m: multiline, i: case-insensitive
    • 等等

typeof operator

检查值的类型,结果是字符串。

当 operand (运算子)无法 resolve 时,结果是"undefined",如何判断是否能 resolve 请参考《Chapter 2. Reference record》

typeof和 spec 中的 Type(x)不同:Type(x)的结果是 Undefined, Null, Boolean, Number, String, or Object 等,不会有 Function 之类的。

Type of operand Result Note
Undefined "undefined"
Null "object" Oops! 历史遗留错误:在面向对象编程中,想 unset 对象时,往往将其赋值为null.
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Object (does not implement [[Call]]) "object" 比如typeof {}, typeof []. 又比如typeof JSON, typeof Math, 不能用作函数.
Object (implements [[Call]]) "function" 比如 typeof function(){}. 又比如typeof String, typeof Object, typeof Array, typeof Function, 能用作函数.
  1. 判断是不是 undefined ,可以用 foo === undefined
  2. 判断是不是 null ,可以用 foo === null
  3. 判断是不是 undefinednull ,可以用 foo == null
  4. 判断是不是一个 Boolean 类型的值,可以用foo === truefoo === false
  5. 判断是不是一个 Number 类型的值,可以用 typeof foo == "number"
  6. 判断是不是一个 String 类型的值,可以用 typeof foo == "string"
  7. 获取准确类型 ( typeof 对 Object 的判断很粗放),可以用 Object.prototype.toString.call(foo).slice(8, -1).toLowerCase()
  8. 判断是不是一个数组,可以用 Array.isArray(foo)
  9. 判断是不是一个函数,可以用 typeof foo == "function"

Type conversion

我们经常看到的 coercion 说的就是 type conversion,但 spec 中并没有 coercion 这个术语。

spec 中算法所用符号的说明:

?表示紧跟的运算可能会抛出异常。

!表示紧跟的运算不会抛出异常 (我都省略掉了。注意:这里的!不是逻辑非运算符)

[] 表示可选。

NewTarget 用来区分new Boolean()Boolean() (这里以 Boolean 类型 举例,后一种方式调用时,NewTarget 是 undefined)

Boolean

Boolean(value)方式被调用时 (前面没有new)

相当于!!value,注意是 2 个逻辑非运算符,通常用!!value而不是Boolean(value), 写起来更简短

如果参数不存在,结果是false

  1. Let b be ToBoolean(value).
  2. If NewTarget is undefined, return b.
  3. ...

ToBoolean(argument) (抽象运算,运算结果是true或者false)

Type of argument Result
Undefined Return false.
Null Return false.
Boolean Return argument.
Number If argument is 0, -0, or NaN, return false; otherwise return true.
String If argument is "" (空字符串), return false; otherwise return true.
Symbol Return true.
Object Return true.

Number

Number(value)方式被调用时 (前面没有new)

相当于+value,不建议使用,影响可读性

如果参数不存在,结果是0

  1. If value is not present, let n be +0.
  2. Else, let n be ? ToNumber(value).
  3. If NewTarget is undefined, return n.
  4. ...

ToNumber(argument) (抽象运算,运算结果是 Number 类型值或者 Exception)

Type of argument Result Note
Undefined Return NaN.
Null Return 0. Oops! 如果是NaN会更合适。
Boolean If argument is true, return 1. If argument is false, return 0.
Number Return argument. (十进制数字)
String 去掉首尾的空白(包括空格,tab,换行等)后,如果是空字符串,return 0, 如果看起来是数字(可能是二进制 Binary,八进制 Octal,十六进制 heXadecimal),return 十进制 decimal 数字 (输入是科学记数法的字符串,输出可能是十进制数字,也可能是科学计数法数字),否则 return NaN. "-0"->-0.
Symbol Throw a TypeError exception.
Object Apply the following steps:
1. Let primValue be ? ToPrimitive(argument, number). (指定了 preferredType)
2. Return ? ToNumber(primValue).

ToPrimitive(input[, preferredType]) (抽象运算)

该运算追求得到 primitive value,如果得不到就会抛出 Exception。
这很好解释了:如果@@toPrimitive 返回的是 Object 类型,直接抛出 Exception 了 (步骤 1-2-6)。

  1. If Type(input) is Object, then
    1. Let exoticToPrim be ? GetMethod(input, @@toPrimitive). (获取Symbol.toPrimitive属性的值,如果属性不存在或属性值是undefinednull,返回undefined,见示例 1;如果是函数,返回函数,见示例 2;否则抛出 TypeError,见示例 3)
    2. If exoticToPrim is not undefined, then
      1. If preferredType is not present, let hint be "default".
      2. Else if preferredType is number, let hint be "number". (我将 spec 中的这一步和下一步对调了,便于记忆)
      3. Else,
        1. Assert: preferredType is string.
        2. Let hint be "string".
      4. Let result be ? Call(exoticToPrim, input, « hint »). (调用函数,见示例 4)
      5. If Type(result) is not Object, return result. (如果不是 Object,返回结果,见示例 4)
      6. Throw a TypeError exception. (如果是 Object 类型,抛出 TypeError,见示例 5)
    3. If preferredType is not present, let preferredType be number.

      这里可以看出 number 的权重比 string 高

    4. Return ? OrdinaryToPrimitive(input, preferredType).
  2. Return input.

When ToPrimitive is called with no preferredType, then it generally behaves as if the preferredType were number.

如果没有 preferredType,那大部分情况下 preferredType 被看成是 number

However, objects may over-ride this behaviour by defining a @@toPrimitive method.

对象通过定义@@toPrimitive 方法可重写这一行为

Date objects over-ride the default ToPrimitive behaviour. Date objects treat no preferredType as if the preferredType were string.

Date 重写了 ToPrimitive 的默认行为。Date 将没有 preferredType 看成是 string。ES spec 中搜索 Date.prototype [ @@toPrimitive ] 就能看出来怎么回事。见下面的示例

const foo = new Date();
// ToPrimitive(foo, 没有 preferredType)
// => "Tue Jun 27 2023 08:49:53 GMT+0800 (China Standard Time)" + 1
foo + 1;
const foo = new Date();
// ToPrimitive(foo, preferredType 是 number)
// => 1687826993053 > 1
foo > 1;

OrdinaryToPrimitive(o, preferredType) (抽象运算)

该运算追求得到 primitive value,如果得不到就会抛出 Exception。
这很好解释了:最后一步直接抛出 Exception 了 (步骤 6)。

  1. Assert: Type(o) is Object.
  2. Assert: preferredType is either number or string.
  3. If preferredType is number, then (我将 spec 中的这一步和下一步对调了,便于记忆)
    1. Let methodNames be « "valueOf", "toString" ».
  4. Else,
    1. Let methodNames be « "toString", "valueOf" ».
  5. For each element name of methodNames, do
    1. Let method be ? Get(o, name). (示例 8)
    2. If IsCallable(method) is true, then (示例 7 中这里是 false)
      1. Let result be ? Call(method, o). (示例 6.a)
      2. If Type(result) is not Object, return result. (示例 6.b)
  6. Throw a TypeError exception. (示例 7)
// 示例 1
// 1. 调用ToNumber(arg), 参数是一个Object类型
// 2. 调用ToPrimitive(arg, number), 虽然定义了Symbol.toPrimitive,但其值是undefined
// 3. 调用OrdinaryToPrimitive(arg, number),先调用valueOf(),结果是对象本身,忽略;
// 再调用toString(),结果是"[object Object]",返回该字符串。
// 4. 调用ToNumber("[object Object]")
Number({
  // 或者这个属性不存在
  [Symbol.toPrimitive]: undefined,
}); // NaN
Number({
  [Symbol.toPrimitive]: null,
}); // 结果同上,原因参考上面

// 示例 2
Number({
  [Symbol.toPrimitive]: () => {
    return 123;
  },
}); // 123

// 示例 3
Number({
  [Symbol.toPrimitive]: 123, // Symbol.toPrimitive属性不是一个函数导致TypeError
}); // TypeError: xxx is not a function

// 示例 4
Number({
  [Symbol.toPrimitive]: (hint) => {
    if (hint === "string") {
      return "abc";
    } else {
      // hint is "number" or "default"
      return 123;
    }
  },
}); // 123. 如果是String(...), 结果就是abc

// 示例 5
Number({
  [Symbol.toPrimitive]: () => {
    return {}; // 返回值不是原始类型导致TypeError
  },
}); // TypeError: Cannot convert object to primitive value

// 示例 6
// 123. 原因:OrdinaryToPrimitive先调用valueOf,结果是一个数组,它是Object类型,忽略;
// 再调用toString,结果是" 123 ",返回该字符串,再调用ToNumber,最终结果是123
Number({
  // 6.a
  valueOf() {
    return [];
  },
  // 6.b
  toString() {
    return " 123 ";
  },
}); // 123

// 示例 7
// TypeError. 原因:valueOf和toString都不是函数,会到最后一步
Number({
  valueOf: 123,
  toString: "abc",
}); // TypeError: Cannot convert object to primitive value

// 示例 8
// TypeError. 原因:这个对象没有valueOf和toString属性,会到最后一步
Number(Object.create(null)); // TypeError: Cannot convert object to primitive value

String

String(value)方式被调用时 (前面没有new):

注意与`${value}`, value + ""之间的比较,三者异同点请看后面

如果参数不存在,结果是""

  1. If value is not present, let s be empty string.
  2. Else,
    1. If NewTarget is undefined and Type(value) is Symbol, return SymbolDescriptiveString(value). (如果 Description 是undefined,结果是"Symbol()",否则是"Symbol(" + value.[[Description]] + ")")
    2. Let s be ? ToString(value).
  3. If NewTarget is undefined, return s.
  4. ...

ToString(argument) (抽象运算,运算结果是 String 类型值或者 Exception)

Type of argument Result Note
Undefined Return "undefined".
Null Return "null".
Boolean If argument is true, return "true". If argument is false, return "false".
Number Return NumberToString(argument). (结果是十进制数字字符串,数字非常大时会是科学记数法的字符串) Oops! -0->"0"
String Return argument.
Symbol Throw a TypeError exception.
Object Apply the following steps:
1. Let primValue be ? ToPrimitive(argument, string). (指定了 preferredType)
2. Return ? ToString(primValue).
// String(value) vs. value + "" vs. `${value}`?
// 相同:三者都用到ToString这个抽象运算

// String(value)单独处理了Symbol类型的值,不会抛异常,后两者都会抛TypeError
String(Symbol()); // "Symbol()"
`${Symbol()}`; // TypeError
Symbol() + ""; // TypeError

const foo = {
  toString() {
    return "abc";
  },
  valueOf() {
    return 123;
  },
};
// 调用了ToString
String(foo); // "abc"
// 调用了ToString
`${foo}`; // "abc"
// 逻辑:先对左右运算子分别调用ToPrimitive,
// 未指定preferredType (其被视为number),valueOf被调用,得到值123;
// 然后应用+运算符,由于右运算子是个字符串,所以会进行string-concatenation
// 首先对左运算子应用ToString,得到"123","123"+""最终结果是"123"
// 具体请参考下面的二元运算符
foo + ""; // "123"

Logical operators (与 ToBoolean 相关)

注意:和其他语言大不相同,因为结果不一定是 Boolean 类型的值

&& (logical and, 逻辑与), || (logical or, 逻辑或)

leftOperand && rightOperandleftOperand || rightOperand

  1. Let lref be the result of evaluating leftOperand.
  2. Let lval be ? GetValue(lref).
  3. Let lbool be ToBoolean(lval).
  4. If lbool is false, return lval.

    如果是||,上面的false 改为 true.
    直接返回左运算子的值,这是 short circuit.

  5. Let rref be the result of evaluating rightOperand.
  6. Return ? GetValue(rref). (返回右运算子的值)

! (logical not, 逻辑非)

!operand

  1. Let ref be the result of evaluating operand.
  2. Let oldValue be ToBoolean(? GetValue(ref)).
  3. If oldValue is true, return false.
  4. Return true.

Conditional operator (与 ToBoolean 相关)

? :

ShortCircuitExpression ? AssignmentExpression : AssignmentExpression

  1. Let lref be the result of evaluating ShortCircuitExpression.
  2. Let lval be ToBoolean(? GetValue(lref)).
  3. If lval is true, then
    1. Let trueRef be the result of evaluating the first AssignmentExpression.
    2. Return ? GetValue(trueRef).
  4. Else,
    1. Let falseRef be the result of evaluating the second AssignmentExpression.
    2. Return ? GetValue(falseRef).

Binary arithmetic operators (主要与 ToNumber 有关)

spec 中并没有 arithmetic operators 这个术语,个人猜测原因是:+还能串接字符串

+ (addition), - (subtraction), * (multiplication), / (division), % (remainder)

会调用:ApplyStringOrNumericBinaryOperator(lval, opText, rval) (抽象运算,简化)

  1. If opText is +, then
    1. Let lprim be ? ToPrimitive(lval).
    2. Let rprim be ? ToPrimitive(rval).
    3. If Type(lprim) is String or Type(rprim) is String, then (用的是 or)
      1. Let lstr be ? ToString(lprim).
      2. Let rstr be ? ToString(rprim).
      3. Return the string-concatenation of lstr and rstr.
    4. Set lval to lprim.
    5. Set rval to rprim.
  2. Let lnum be ? ToNumber(lval).
  3. Let rnum be ? ToNumber(rval).
  4. Return the result of applying the above operation to lnum and rnum.

Unary arithmetic operators (与 ToNumber 有关)

++ (increment operator, 分为 postfix increment operator 和 prefix increment operator), -- (decrement operator,分为 postfix 和 prefix)

以 postfix increment operator 为例:

LeftHandSideExpression++

  1. Let lhs be the result of evaluating LeftHandSideExpression.
  2. Let oldValue be ? ToNumber(? GetValue(lhs)).
  3. Let newValue be the result of adding the value 1 to oldValue, using the same rules as for the + operator.
  4. Perform ? PutValue(lhs, newValue).
  5. Return oldValue.
let i = "abc";

// 不要简单的将i++看成是i = i + 1
i++; // i is NaN

Relational operators (主要与 ToNumber 有关)

< (less than), <= (less than or equals), > (greater than), >= (greater than or equals)

会调用:Abstract Relational Comparison (抽象运算,简化)

  1. Let lprim be ? ToPrimitive(lval, number). (指定 preferredType, 这里可以看出这些运算符是期待 Number 类型的值)
  2. Let rprim be ? ToPrimitive(rval, number). (指定 preferredType)
  3. If Type(lprim) is String and Type(rprim) is String, then (用的是and)
    1. Return the result of string-comparision. (字符串比较结果)
  4. Else,
    1. Let lnum be ? ToNumber(lprim).
    2. Let rnum be ? ToNumber(rprim).
    3. If lnum or rnum is NaN, return undefined. (调用方会将undefined视为false)
    4. Return the result of number-comparision. (数字比较结果)

Equality operators

== (double equals), !=, === (triple equals), !==

=====如何选择?
==可以考虑用在:typeof == "string" (typeof运算结果一定是 String 类型), foo == null (用来检查 foo 是null或者undefined)。其他情况推荐使用===

==!=会用到 Abstract Equality Comparison (抽象运算,简化)

  1. If Type(x) is the same as Type(y), then
    1. Return x === y.
  2. If x is undefined and y is null, return true.
  3. If x is null and y is undefined, return true.
  4. If Type(x) is Boolean, return ToNumber(x) == y.
  5. If Type(y) is Boolean, return x == ToNumber(y).
  6. If Type(x) is Number and Type(y) is String, return x == ToNumber(y).
  7. If Type(x) is String and Type(y) is Number, return ToNumber(x) == y.
  8. If Type(x) is either Number, String, or Symbol and Type(y) is Object, return x == ? ToPrimitive(y).
  9. If Type(x) is Object and Type(y) is either Number, String, or Symbol, return ? ToPrimitive(x) == y.
  10. Return false.

===!==会用到 Strict Equality Comparison (抽象运算,简化)

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Number, then
    1. If x is NaN, return false.
    2. If y is NaN, return false.
    3. If x is +0 and y is -0, return true.
    4. If x is -0 and y is +0, return true.
    5. If x is the same as y, return true. (注意进制不同,比如0xa10是一样的)
    6. Return false.
  3. Return SameValueNonNumber(lval, rval).

SameValueNonNumber(x, y) (抽象运算)

  1. Assert: Type(x) is not Number.
  2. Assert: Type(x) is the same as Type(y).
  3. If Type(x) is Undefined, return true.
  4. If Type(x) is Null, return true.
  5. If Type(x) is Boolean, then
    1. If x and y are both true or both false, return true; otherwise, return false.
  6. If Type(x) is String, then
    1. If x and y are exactly the same sequence of code units (same length and same code units at corresponding indices), return true; otherwise, return false.
  7. If Type(x) is Symbol, then
    1. If x and y are the same Symbol value, return true; otherwise, return false. (注意 Symbol 类型值的唯一性)
  8. If x and y are the same Object value, return true. Otherwise, return false.
const foo = null;
const bar = 0;

foo >= bar; // true
foo > bar; // false
foo == bar; // false

// 数学常识:如果左运算子大于或者等于右运算子,那么前者要么大于后者,要么等于后者。
// 注意:左右运算子都是数字。
// 粗看上述代码,会觉得它们违背了上述数学常识,但只要熟悉 >= > == 三个运算符的算法,
// 就不会对上述结果感到惊讶。
// 如果一开始将foo和bar都赋值为数字,就和上述数学常识匹配了。
"" == 0; // true,对''进行了ToNumber转换,变成0
0 == " "; // true,对' '进行了ToNumber转换,变成0
"" == " "; // false,都是String类型,会进行 ===

const a = new String("foo");
const b = "foo";
const c = new String("foo");
// true,对a进行了ToPrimitive转换(注意:valueOf不是对象本身,而是"foo",参考`String.prototype.valueOf`),
// 变成"foo", 然后进行 ===
a == b;
b == c; // true,对c进行了ToPrimitive转换,变成"foo", 然后进行 ===
a == c; // false,进行 ===

// 数学常识:如果a=b, b=c,那么a=c,这称为等式的传递性。注意:左右运算子都是数字。
// 粗看上述2段代码,会觉得它们违背了上述数学常识,但只要熟悉 == 运算符的算法,
// 就不会对上述结果感到惊讶。

SameValue(x, y) (抽象运算,Object.is(x, y)会用到它)

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Number, then
    1. If x is NaN and y is NaN, return true.
    2. If x is +0 and y is -0, return false.
    3. If x is -0 and y is +0, return false.
    4. If x is the same as y, return true.
    5. Return false.
  3. Return SameValueNonNumber(x, y).

SameValueZero(x, y) (抽象运算,String.prototype.includes, Array.prototype.includes, Map, Set会用到它)

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Number, then
    1. If x is NaN and y is NaN, return true.
    2. If x is +0 and y is -0, return true.
    3. If x is -0 and y is +0, return true.
    4. If x is the same as y, return true.
    5. Return false.
  3. Return SameValueNonNumber(x, y).

Misc 1. Date time.md

Date time

ISO 8601 format

ISO 8601 是表达日期和时间的一种国际标准,完整体格式: YYYY-MM-DDTHH:mm:ss.SSS[UTC offset]

  1. YYYY: Year, 4 位
  2. MM: Month, 2 位, 01 - 12
  3. DD: Day, 2 位, 01 - 31
  4. T 或者 空格: date 和 time 的分隔符, T 意味着 Time 开始,可以用空格替代 T,但建议用 T
  5. HH: Hour, 2 位, 00 - 23. 注意: ISO 8601 用的是小写 hh, 因为它是 24 小时制 (dayjs 中 大写 HH 表示 24 小时制, 小写 hh 表示 12 小时制,我采用 HH)
  6. mm: Minute, 2 位, 00 - 59
  7. ss: Second, 2 位, 00 - 59
  8. SSS: milliSecond, 3 位, 000 - 999. 注意: ISO 8601 没有规定必须用 S (我采用 dayjs 的大写 S, 与小写 s 进行区分), 也没有规定必须 3 位 (我采用 EcmaScript 的 3 位)
  9. UTC offset: 准确来说是 Time Zone Designator (TDZ) 时区标识, 可以不存在, 可以是 Z, 可以是 +hh:mm 或者 -hh:mm, 一共有 24 个时区: -11 ... -1 Z +1 ... +12

注意:以下几个 time 都是 string

  • local time: UTC offset 不存在, 说的是不同 time zone 下的同一时间点
    • e.g. 2023-07-01T02:03:04.567
  • UTC time: UTC offset 是 0, 用 Z 表示
    • e.g. 2023-07-01T02:03:04.567Z
  • UTC offset time
    • e.g. 2023-07-01T02:03:04.567+08:00

原生 Date object

原生 Date object 内部存储的永远UTC 整数值 (精确到 3 位毫秒), 起始点是 1970-01-01T00:00:00.000Z. e.g. new Date('1970-01-01T08:00:00.123+08:00') 这个对象的 valueOf() 结果是 123

非常重要:虽然内部存储的是 UTC 日期时间,但我们使用的是转换后的 local 日期时间

  • string 值 -> Date 对象
    • new Date(stringArg), 记得让 stringArg 符合 ISO 8601 format, 请勿使用 date-only forms (比如 "2023-07-01"), 会被看成是 UTC time, 而不是 local time, 而且不同 EcmaScript 实现还不相同
  • Date 对象 -> string
    • toISOString(), 结果是 UTC time

第三方库 dayjs

Dayjs 对象是原生 Date 对象的 wrapper.

非常重要:虽然内部存储的是 UTC 日期时间,但我们使用的是转换后的 local 日期时间

  • string 值 -> Dayjs 对象
    • dayjs(stringArg), stringArg 符合 ISO 8601 format 时用
    • dayjs(stringArg, <customParseFormat>), stringArg 不符合 ISO 8601 format 时用,使用前需要引入 customParseFormat, 请看《实战 - 问题 1 和 2》
  • Dayjs 对象 -> string
    • toISOString(), 结果是 UTC time
    • format([template]), 结果是 UTC offset time 或 UTC time, 可以不输入 template, 默认 template 是 YYYY-MM-DDTHH:mm:ssZ, 特别注意:这里的Z并不是去获取 UTC time,它说的是要保留时区信息,具体看《实战 - 问题 3》

实战

在线 playground

问题 1: 后端返回一个 UTC time 或 UTC offset time, 要求前端把它看成是 local time,如何处理?e.g. 后端返回的是 2023-07-01T02:03:04Z,前端应该将看成是 2023-07-01T02:03:04
思路 1: 要点是如何丢掉时区?因为后端返回的字符串可能是Z结尾,也可能是+02:00之类的结尾
答案 1: dayjs(backendString, 'YYYY-MM-DDTHH:mm:ss'), 需要特别注意的是:需要引入 customParseFormat, 不然 dayjs 会进行时区换算, 那样的话结果就不对了

问题 2: 后端返回08:00, 前端需要在 antd 时间组件中显示出来,如何做?
答案 2: dayjs('08:00', 'HH:mm'), 需要特别注意的是:需要引入 customParseFormat, 不然无法得到一个有效的 Dayjs 对象

问题 3: dayjs('2023-07-01T02:03:04+01:00').format('YYYY-MM-DD HH:mm')dayjs('2023-07-01T02:03:04+01:00').format('YYYY-MM-DD HH:mmZ') 有什么区别?
思路 3: 要点是dayjs('2023-07-01T02:03:04+01:00')到底是什么?format 中的结尾处一个没有 Z,一个有 Z,有什么区别?
答案 3: 具体过程如下

  1. dayjs('2023-07-01T02:03:04+01:00')得到一个 Dayjs 对象
  2. 它存储的是 2023-07-01T01:03:04Z 这个 UTC time 的整数值
  3. 需要注意的是,我们实际上操作的是当地时区的日期时间,假如我们在东 8 区,这个 Dayjs 对象就是 2023-07-01T09:03:04+08:00
  4. 然后调用对象的 format 方法 - 1st YYYY-MM-DD HH:mm,从左到右解析分别得到 2023 - 07 - 01 (空格) 09 : 03 (没有ss丢掉秒) (没有SSS丢掉毫秒) 没有Z丢掉时区,结果就是 2023-07-01 09:03
  5. 2nd YYYY-MM-DD HH:mmZ,从左到右解析分别得到 2023 - 07 - 01 (空格) 09 : 03 (没有ss丢掉秒) (没有SSS丢掉毫秒) +08:00,结果就是 2023-07-01 09:03+08:00

Chapter 3. Environment record (scope)

Chapter 3. Environment record (scope)

Environment record 对应的是 scope

Environment record is used to explain the behavior of name resolution in nested functions and blocks.

Environment record 是一个抽象概念,可以将其理解为一个 plain object.

name resolution 也就是 identifier resolution. name 包括 variable name, function name, parameter name, class name 等。

Spec 经常用到 binding 这个词,a binding is the association between an identifier and a value.

根据代码类型 (比如 global code, module code, function code 等)将 environment record 粗略分为:

  1. Global environment record (由 object environment record + declarative environment record 组成)

    • [[OuterEnv]] is null.

    • bindings for the top-level declarations. (全局中最上层声明的所有绑定)

      object environment record 关联一个 binding object,即 the global object, 它包括:global built-in bindings (比如console)和 VariableDeclaration (指的是用var声明), FunctionDeclaration (函数声明), AsyncFunctionDeclaration (异步函数声明)等。

      declarative environment record 包括: LexicalDeclaration (指的是用letconst声明), ClassDeclaration (类声明).

      // Legacy variable using `var`, added to object environment record.
      var x = 10;
      // Modern variable using `const`, added to declarative environment record.
      const y = 20;
      
      console.log(x, y); // 10 20
      // in global environment, `this` is the global object
      // which is the binding object of object environment record.
      console.log(this.x, this.y); // 10 undefined
      
      this.x = 1;
      this.y = 2;
      
      console.log(x, y); // 1 20
    • this is the global object.

      • window for browsers, self for web workers
      • global for node.js
      • globalThis for all

    Global environment record is used to represent the outer most scope that is shared by all of the ECMAScript script elements.

    所有 script 元素共享 global environment record。

    下面这段代码说明:一个 script 中出现 exception 只会阻断该 script 中后续代码的执行,不会阻断其他 script 代码的执行。所有 script 共享 global environment record,随着 script 的执行,global environment record 在变化。

    <script>
      console.log(x); // ReferenceError: x is not defined
      console.log(123); // 不会打印123
    </script>
    <script>
      let x = 1;
      const y = 10;
    </script>
    <script>
      console.log(x); // 1
      x = 2;
      console.log(x); // 2
    
      console.log(y); // 10
      y = 20; // TypeError: Assignment to constant variable.
    </script>
  2. Module environment record (一种 declarative environment record)

    • [[OuterEnv]] is global environment record.

    • bindings for the top-level declarations. (模块中最上层声明的所有绑定)

      包括 VariableDeclaration (指的是用var声明), FunctionDeclaration, AsyncFunctionDeclaration, LexicalDeclaration (指的是用letconst声明), ClassDeclaration 等.

    • this is undefined.

  3. Function environment record (一种 declarative environment record)

    每次执行函数会生成新的environment record.

    • [[OuterEnv]]的情况:

      创建函数时会确定它的[[Environment]](即:将函数声明所在的 environment record 存入它的[[Environment]],请参考 spec 中的 OrdinaryFunctionCreate。[[Environment]]是函数对象的一个 internal slot)。执行函数时,新生成的 environment record 的[[OutEnv]]被设置为[[Environment]]。

    • bindings for the top-level declarations. (函数中最上层声明的所有绑定)

      包括 VariableDeclaration (指的是用var声明), FunctionDeclaration, AsyncFunctionDeclaration, LexicalDeclaration (指的是用letconst声明), ClassDeclaration 等.

    • this的情况:

      创建函数时会确定它的[[ThisMode]](请参考 spec 中的 OrdinaryFunctionCreate,[[ThisMode]]是函数对象的一个 internal slot),其值是 lexical, strict, or gloabl.

      • lexical means that this refers to the this value of a lexically enclosing function. (这里用 environment 更恰当,比如 module 中的 arrow function)

      arrow function 的[[ThisMode]]是 lexical.

      • strict means that this value is used exactly as provided by an invocation of the function.

      strict mode 下的 function declaration 和 function expression 的[[ThisMode]]是 strict.

      • global means that a this value of undefined or null is interpreted as a reference to the global object, and any other this value is first passed to ToObject.

      non-strict mode 下的 function declaration 和 function expression 的[[ThisMode]]是 global.

      确定this的具体算法会用到 EvaluateCall(func, ref, arguments, tailPosition) (抽象运算):

      The abstract operation EvaluateCall takes arguments func (an ECMAScript language value), ref (an ECMAScript language value or a Reference Record), arguments (a Parse Node), and tailPosition (a Boolean).

      1. If ref is a Reference Record, then
        1. If IsPropertyReference(ref) is true, then

          以 property accessor 形式调用函数,thisValue 是 ref.[[Base]],见示例 1 和 2

          1. Let thisValue be GetThisValue(ref).
        2. Else,

          以 identifier 形式调用函数,thisValue 是undefined (注意这是排除了 with statement 的情况,排除原因是其不被推荐使用),见示例 3

          1. Let refEnv be ref.[[Base]].
          2. Assert: refEnv is an Environment Record.
          3. Let thisValue be refEnv.WithBaseObject(). (如果是 with statement,thisValue 是 with object,否则 thisValue 是undefined)
      2. Else,

        以其他形式调用函数,比如:IIFE,thisValue 是undefined,见示例 4 和 5

        1. Let thisValue be undefined.
      3. Let argList be ? ArgumentListEvaluation of arguments.
      4. If Type(func) is not Object, throw a TypeError exception.
      5. If IsCallable(func) is false, throw a TypeError exception.
      6. If tailPosition is true, perform PrepareForTailCall().
      7. Let result be Call(func, thisValue, argList).

        这里会调用 Function Object 的[[Call]],它又会调用 OrdinaryCallBindThis(F, calleeContext, thisArgument),它会根据 Function Object 的[[ThisMode]]来绑定 this value

        [[ThisMode]]的值:

        1. 如果是 lexical,直接返回 (意思是不进行最后一步的绑定操作,所以说 arrow function environment record 不绑定 this value)
        2. 如果是 strict,thisValue 就是 thisArgument
        3. 其他,即:global,如果 thisArgument 是undefinednull,thisValue 就是 the global object, 否则 thisValue 就是 ToObject(thisArgument)
        4. ... (最后一步操作是为 environment record 绑定 this value)
      8. ... (忽略了后续步骤)
      const foo = {
        bar: function () {
          console.log(this);
        },
      };
      
      // 示例 1
      // Reference record:
      // {[[Base]]: foo指向的对象, [[ReferencedName]]: 'bar', [[Strict]]: false}
      foo.bar(); // foo, the dot notation
      foo['bar'](); // foo, the bracket notation
      // 示例 2
      // Reference record:
      // {[[Base]]: 临时对象, [[ReferencedName]]: 'bar', [[Strict]]: false}
      ({
        x: 1,
        bar: function () {
          console.log(this.x); // 1
        },
      }).bar();
      
      const bar = foo.bar;
      // 示例 3
      // Reference record:
      // {[[Base]]: globalEnvRec, [[ReferencedName]]: 'bar', [[Strict]]: false}
      bar(); // the global object (如果是在strict mode下,结果是undefined)
      
      // 示例 4
      // 没有Reference record
      // the global object (如果是在strict mode下,结果是undefined)
      (false || foo.bar)();
      
      // 示例 5
      // 没有Reference record
      // the global object (如果是在strict mode下,结果是undefined)
      (function () {
        console.log(this);
      })();
      
      // 示例 6
      this.x = 10;
      const qux = {
        x: 20,
        bar: () => console.log(this.x),
        baz() {
          let arrow = () => console.log(this.x);
          return arrow();
        },
      };
      
      // qux.bar指向的函数的[[ThisMode]]是lexical, 它的this会refer to外部环境的this,
      // 外部环境是global environment record,它的this是the global object
      qux.bar(); // 10
      // arrow指向的函数的[[ThisMode]]是lexical,它的this会refer to外部环境的this,
      // 也就是baz环境的this,接着看baz环境怎么生成的?Reference record:
      // {[[Base]]: qux指向的对象, [[ReferencedName]]: 'baz', [[Strict]]: false}
      qux.baz(); // 20

浏览器的setTimeout, setInterval调用 handler 的方式是handler.call(window),而不是handler(). (参考:https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers).

'use strict';
const user = {
  firstName: 'John',
  sayHi() {
    console.log(`Hi, I am ${this.firstName}.`);
  },
};

// 如果setTimeout不做特殊处理,这里会报TypeError: cannot read properties of undefined.
setTimeout(user.sayHi, 1000); // Hello, I am undefined.

// 如何让`this`是user指向的对象呢?
const boundSayHi = user.sayHi.bind(user);
setTimeout(boundSayHi, 1000); // Hello, I am John.

生成 environment record 实际上是在 instantiation (实例化),这个过程是在代码执行前进行的。步骤如下:

基于 spec 的 GlobalDeclarationInstantiation 和 FunctionDeclarationInstantiation 的修改

parse 阶段 (参考《Misc》)会收集所有 FunctionDeclaration, ParameterName, VariableDeclaration, LexicalDeclaration 等集合

  1. LexicalDeclaration 集合自身内有重名,直接报 SyntaxError
  2. LexicalDeclaration 集合与其他集合有重名,直接报 SyntaxError

弄清楚以下几个步骤,hoisting 就很容易理解,spec 中并不存在 hoisting 这个术语。

  1. FunctionDeclaration list (AsyncFunctionDeclaration 等属于这一类),针对每个元素:

    • 如果遇到重名的情况,用最后一个声明
    • 在 environment record 中创建 binding,初始化为函数对象
  2. FormalParameterName list,针对每个元素:

    function environment record 才会进行这个步骤

    • 如果遇到与上面 list 重名的情况
      • 如果 FormalParameterName 集合中有默认值的情况,用该声明覆盖之前的声明(此处简单处理了,实际上要复杂很多,这里会创建一个单独的 environment record)
      • 否则,忽略该声明
    • 如果遇到重名的情况,忽略该声明
    • 在 environment record 中创建 binding,如果未提供实参,初始化为undefined,否则初始化为实参
  3. VariableDeclaration list (指的是用var声明),针对每个元素:

    • 如果遇到与上面 list 重名的情况,忽略该声明
    • 如果遇到重名的情况,忽略该声明
    • 在 environment record 中创建 binding,初始化为undefined
  4. LexicalDeclaration list (指的是用letconst声明,ClassDeclaration 属于这一类),针对每个声明:

    • 在 environment record 中创建 binding,不进行初始化,进入 Temporal Dead Zone (TDZ,在初始化前不能 access 标识符,否则报 ReferenceError)
function test(a, b) {
  var x = 10;

  let y;

  const z = 20;

  function foo() {}

  var bar = function _bar() {};

  (function baz() {});
}

test(1);

Function environment record instantiation and execution
图片显示不出来,请看原文最下方:https://github.com/ShawLocke/ES-blog/blob/main/posts/Chapter%203.%20Environment%20record%20(scope).md

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.