JS冷门知识点(二)

64 阅读8分钟

以下是我在工作中经常忘记,或者总是想不起使用的一些知识点总结,如有错误欢迎指正

1. es6 使用对象字面量创建一个对象时,支持"[]"中括号的方式创建key

key 可以是表达式,此种方式叫做 “计算属性”,且对象的 key 只能是字符串或者 symbol


let fruit = 'foo';

let bag = {

  [fruit]: 5, // 属性名是从 fruit 变量中得到的

};

bag.foo == 5;

2. 构造器模式测试:new.target

new.target 在一个函数内部,我们可以使用 new.target 属性来检查它是否被使用 new 进行调用了

new.target语法由一个关键字"new",一个点,和一个属性名"target"组成。通常"new."的作用是提供属性访问的上下文,但这里"new."其实不是一个真正的对象。不过在构造方法调用中,new.target指向被new调用的构造函数,所以"new."成为了一个虚拟上下文


function User() {

    alert(new.target);

}

// 不带 "new":

User(); // undefined

// 带 "new":

new User(); // function User { ... }

基于此,我们可以让 new 调用和常规调用做相同的工作


function User(name) {

    if (!new.target) { // 如果你没有通过 new 运行我

        return new User(name); // ……我会给你添加 new

    }

    this.name = name;

}

let john = User("John"); // 将调用重定向到新用户

alert(john.name); // John

当一个父类构造方法在子类构造方法中被调用时,情况与之相同


class A {

    constructor() {

        console.log(new.target.name);

    }

}

class B extends A {

    constructor() {

        super();

    }

}

var a = new A(); // logs "A"

var b = new B(); // logs "B"

class C {

    constructor() {

        console.log(new.target);

    }

}

class D extends C {

    constructor() {

        super();

    }

}

var c = new C(); // logs class C{constructor(){console.log(new.target);}}

var d = new D(); // logs class D extends C{constructor(){super();}}

3. 可选链 ?.

?. 检查左边部分是否为 null/undefined,如果不是则继续运算。它不是一个运算符,而是一个特殊的语法结构。它可以与函数和方括号一起使用

obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined。

obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined。

obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined。

例如:userAdmin.admin?.(); user1?.[key];

此外,我们还可以将 ?.delete 一起使用:

delete user?.name; // 如果 user 存在,则删除 user.name

注意:我们可以使用 ?. 来安全地读取或删除,但不能写入

let user = null;

user?.name = "John"; // Error,不起作用

// 因为它在计算的是:undefined = "John"

4. Symbol 是带有可选描述(name)的“原始唯一值”,属于基本类型

Symbol 是 带有可选描述(name)的“原始唯一值” ,属于 基本类型 。且不能自动转换为字符串,需要调用.toString() (JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert 任何值,都可以生效)

symbol 属性不参与 for..in 循环。

Object.keys(user) 也会 忽略 它们

Object.assign 会同时 复制字符串和 symbol 属性

symbol 总是不同的值 ,即使它们有相同的名字。如果我们希望同名的 symbol 相等,那么我们应该使用 全局注册表Symbol.for(key) 返回(如果需要的话则创建)一个以 key 作为名字的全局 symbol 。使用 Symbol.for 多次调用 key 相同的 symbol 时,返回的就是同一个 symbol。

Symbol.keyFor(),可以反向通过值获取描述(name),只能获取全局注册表(Symbol.for)中创建的值 。非全局的可以使用 description 属性


// 通过 name 获取 symbol

let globalSymbol = Symbol.for("name");

let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // name,全局 symbol

alert( Symbol.keyFor(localSymbol) ); // undefined,非全局

alert( localSymbol.description ); // name

symbol 有两个主要的 使用场景

1、“隐藏” 对象属性

如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 symbol 并使用它作为属性的键。symbol 属性不会出现在 for..in 中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的 symbol。因此,该属性将受到保护,防止被意外使用或重写。

因此我们可以使用 symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。

2、JavaScript 使用了许多系统 symbol

这些 symbol 可以作为 Symbol.* 访问。我们可以使用它们来改变一些内建行为。例如,我们可以使用 Symbol.iterator 来进行 迭代 操作,使用 Symbol.toPrimitive 来设置 对象原始值的转换 等等。

从技术上说,symbol 不是 100% 隐藏的。有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们 获取所有的 symbol 。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有键 ,包括 symbol。但大多数库、内建方法和语法结构都没有使用这些方法。

5. 对象的比较(原始值转换)

1). 对象不能进行数学运算,如 obj1 + obj2,或 obj1 > obj2 等,此种运算都会首先将对象转为原始值,在进行

2). 对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。

3). 对象转换为boolean,始终为true

这里有 三种类型(hint,提示) 转换:

"string":对于 alertanotherObj[obj] 和其他需要字符串的操作

"number":主要是对于数学运算

"default":少数运算符,通常对象以和 "number" 相同的方式实现 "default" 转换

例如

1). 二元加法 可用于字符串(连接),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据 "default" hint 来对其进行转换。

此外,如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default" hint。

2). 像 < 和 > 这样的小于/大于 比较运算符 ,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default”。这是 历史原因

规范明确描述了哪个运算符使用哪个 hint,转换算法是

  1. 优先调用 obj[Symbol.toPrimitive] (hint) 如果这个方法存在

  2. 如果不存在 ,则:

1). 如果 hint 是 "string" 尝试调用 obj.toString()obj.valueOf(),无论哪个存在(优先调用前者)

2). 如果 hint 是 "number" 或者 "default"

尝试调用 obj.valueOf()obj.toString(),无论哪个存在(优先调用前者)

所有 这些方法 都必须 返回一个原始值 才能工作

obj[Symbol.toPrimitive] (hint) 不存在,obj.toString()obj.valueOf() 其中一个返回了非原始值,则忽略,调用另一个函数。如两个都返回非原始值,使用时,则会报错

默认情况下,普通对象 具有 toString(该方法应该返回对象的“人类可读”表示,可用于日志记录或调试) 和 valueOf 两个原型方法,如没有实现将默认选择原型方法调用:

toString 方法返回一个字符串 "[object Object]"。

valueOf 方法返回对象自身。

注意: 只是普通对象有这两个函数,像数组对象就只有 toString ,没有另外的valueOfSymbol.toPrimitive


// 测试 Symbol.toPrimitive

let user = {

    name: "John",

    money: 1000,

    [Symbol.toPrimitive](hint) {

        alert(`hint: ${hint}`);

        return hint == "string" ? `{name: "${this.name}"}` : this.money;

    }

};

// 转换演示:

alert(user); // hint: string -> {name: "John"}

alert(+user); // hint: number -> 1000

alert(user + 500); // hint: default -> 1500


// 测试 toString/valueOf

let user = {

    name: "John",

    money: 1000,

    // 对于 hint="string"

    toString() {

        return `{name: "${this.name}"}`;

    },

    // 对于 hint="number" 或 "default"

    valueOf() {

        return this.money;

    }

};

alert(user); // toString -> {name: "John"}

alert(+user); // valueOf -> 1000

alert(user + 500); // valueOf -> 1500


// 测试全部返回非原始值

let user = {

    name: "John",

    money: 1000,

    // 对于 hint="string"

    toString() {

        return {};

    },

    // 对于 hint="number" 或 "default"

    valueOf() {

        return {};

    }

};

alert(user); // Uncaught TypeError: Cannot convert object to primitive value

alert(+user); // Uncaught TypeError: Cannot convert object to primitive value

alert(user + 500); // Uncaught TypeError: Cannot convert object to primitive value


// 测试部分返回非原始值

let user = {

    name: "John",

    money: 1000,

    // 对于 hint="string"

    toString() {

        return {};

    },

    // 对于 hint="number" 或 "default"

    valueOf() {

        return this.money;

    }

};

alert(user); // toString 返回了非原始值,所以调用了valueOf -> 1000

alert(+user); // valueOf -> 1000

alert(user + 500); // valueOf -> 1500


//测试自动调用原型方法

let user = {

    name: "John",

    money: 1000,

    // 对于 hint="number" 或 "default"

    valueOf() {

        return this.money;

    }

};

// hint 为 string,虽然没有定义 toString 方法,但调用了原型中的方法

alert(user); // toString -> [object Object]

6. 使用原始类型包装器创建变量,不要使用new

原始类型使用 包装器string,number,bigint,boolean,symbol)创建时,不要使用new ,否则返回的是一个对象,会产生一些副作用,平时除了转换格式,直接用字面量就好


typeof (new Number(0)) == 'object' //true

let zero = new Number(0);

if (zero) { // zero 为 true,因为它是一个对象

   alert( "zero is truthy?!?" );

}

7. 原始值(基本类型)只是一个值,他不能存储额外数据

虽然原始值可以调用一些方法,如:str.toUpperCase(),但这是 javascript引擎的特殊处理 ,并高度优化了这一过程。理论上原始值还是一个值,并不附带其它属性。

以下是 str.toUpperCase() 中实际发生的情况:

  1. 字符串 str 是一个原始值。因此,在访问其属性时,会使用对应的原始对象包装器创建一个包含字符串字面值的特殊对象,并且具有可用的方法,例如 toUpperCase()。

  2. 该方法运行并返回一个新的字符串。

  3. 特殊对象被销毁,只留下原始值 str。

所以你在给原始值添加一些其它属性时会不起作用,因为特殊对象已经销毁了


let str = "Hello";

str.test = 5; // * 如果是严格模式,此行报错:Uncaught TypeError: Cannot create property 'test' on string 'Hello'

alert(str.test); //如果是非严格模式,将输出 undefined

为什么?让我们看看在 * 那一行到底发生了什么:

  1. 当访问 str 的属性时,一个“对象包装器”被创建了。

  2. 在严格模式下,向其写入内容会报错。

  3. 否则,将继续执行带有属性的操作,该对象将获得 test 属性,但是此后,“对象包装器”将消失,因此在最后一行,str 并没有该属性的踪迹。

这个例子清楚地表明,原始类型不是对象,它们不能存储额外的数据。

参考

JavaScript 编程语言

MDN