JavaScript基础之 对象(二)

146 阅读5分钟

构造器和操作符

构造函数

使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。
构造函数在技术上是常规函数,不过它有两个规定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由"new"操作符实现。
function User(name){
    this.name = name;
    this.isAdmin = false;
}

let user = new User("Jack:);

当一个函数被使用new操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给this
  2. 函数体执行。通常它会修改this,为其添加新的属性。
  3. 返回this的值。

换句话说,new User(...)做的就是类似的事情:

function User(name){
    // this = {};  (隐式创建)
    
    // 添加属性到this
    this.name = name;
    this.isAdmin = false;
    
    // return this;  (隐式返回)
}

现在,如果我们像创建其他用户,我们可以调用new User("Alice")等。比每次都是用字面量创建要短得多,而且更易于阅读。

这是构造器的主要目的——实现可重用的对象创建代码。

让我们在强调一遍——从技术上讲,任何函数(除了箭头函数,他没有自己的this)都可以用作构造器。即可以通过new来运行,它会执行上面的算法。"首字母大写"是一个共同的约定,以明确表示一个函数将被使用new来运行。

⭐ new function(){...}
如果我们有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中,像这样:

let user = new function(){
   this.name = "John:";
   this.isAdmin = false;
   // 用于用户创建的其他代码
   // 也许是复杂的逻辑和语句
   // 局部变量等
};

构造器的return

通常,构造器没有return语句,它们的任务是将所有必要的东西写入this,并自动转换为结果。 如果这有一个return语句,那么:

  1. 如果return返回的是一个对象,则返回这个对象
  2. 如果return返回的是一个原始类型,则忽略 带有对象的return返回该对象,在所有其他情况下返回this
// 返回一个对象
function BigUser(){
    this.name = "John";
    return { name: "Godzilla" }
}

alert( new BigUser().name ); // Godzilla
// /返回原始类型或空
function SmallUser(){
    this.name = "John";
    return; // return 0;等原始类型
}

alert( new SmallUser().name ) // John

构造器中的方法

我们不仅可以将属性添加到this中,还可以添加方法。

function User(name){
    this.name = name;
    
    this.sayHi = function(){
        alert(`My name is ${this.name}`);
    }
}

let john = new User("John");
john.sayHi(); // My name is John

原文链接:zh.javascript.info/constructor…

Symbol类型

根据规范,对象的属性键只能是字符串类型或者Symbol类型。

Symbol

"Symbol"值表示唯一的标识符。 可以使用Symbol()来创建这种类型的值:

// id 是 Symbol 的一个实例化对象
let id = Symbol();

创建时,我们可以给Symbol一个描述(也成为Symobl名),这在代码调试时非常有用:

// id 是描述为 “id” 的Symbol
let id = Symbol("id")

Symbol不会被自动转换为字符串
JavaScript中的大多数值都支持字符串的隐式转换。例如,我们可以alert任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。 例如,这个alert将会提示出错:

let id = Symbol("id");
alert(id); // Uncaught TypeError: Cannot convert a Symbol value to a string

这是一种防止混乱的“语言保护”,因为字符串和Symbol有本质上的不同,不应该意外地将它们转换成另一个。
如果我们真的想显示一个Symbol,我们需要在它上面d调用.toString(),或者获取symbol.description属性,是现实描述

let id = Symbol("id");
alert(id.toString()); // Symbol(id)
alert(id.description); // id

"隐藏"属性

Symbol允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。
例如,如果我们使用的是属于第三方代码的user对象,我们想要给他们添加一些标识符。
我们可以给他们使用Symbol键:

let user = {
    name: "John"
};

let id = Symbol("id");
user[id] = 1;
alert( user[id] ) // 1

如果我们要在对象字面量{...}中使用Symbol,则需要使用方括号把它括起来。

let id = Symbol("id");

let user = {
    name: "John",
    [id]: 123
}

使用Symbol("id")作为键,比起用字符串"id"来有什么好处呢?

  • 因为user对象属于其他的代码,我们不应该直接添加任何字段,这样很不安全。但是添加的Symbol属性不会被意外访问到,第三方代码根本不会看到它,所以使用Symbol基本上不会有问题。
  • 另外,另一个脚本希望在user中有自己的标识符,以实现自己的目的。这可能是另一个JavaScript库,因此脚本之间完全不了解彼此。因为Symbol总是不同的,即使它们有相同的名字。我们的标识符和它们的标识符之间不会有冲突。
let user = { name: "John" };

// 我们的脚本
user.id = "Our id value";

// 另一个脚本
user.id = "Their id value";

// id被另一个脚本重写了!!!

Symbol属性不参与for...in中,会被跳过

let id = Symbol("id");

let user = {
    name: "John",
    age: 18,
    [id]: 123
};

for(let key in user) alert(key); // name age

// 使用Symbol直接访问
alert(user[id]); // 123

Object.keys(user)也会忽略它们。只是一般“隐藏符号属性”原则的一部分,如果另一个脚本或库遍历我们的对象,他不会意外地访问到符号属性

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

let id = Symbol("id");
let user = {
    [id]: 123
};

let clone = Object.assign({}, user);

alert(clone[id]);

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

全局Symbol

有时,我们想要名字相同的Symbol具有相同的实体。例如,应用程序的不同部分想要访问Symbol("id")指的是完全相同的属性。
为了实现这一点,这里有一个全局Symbol注册表。我们可以在其中创建Symbol并在稍后访问它们,它可以确保每次访问相同名字的Symbol时,返回的都是相同的Symbol
要从注册表中读取(不存在则创建)Symbol,请使用Symbol.for(key)

// 从全局注册表中读取
let id = Symbol.for("id") //如果该Symbol不存在,则创建

// 再次读取
let idAgain = Symbol.for("id");

alert(id === idAgain) // true

使用Symbol.keyFor(sym),通过全局Symbol返回一个名字

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

alert( Symbol.keyFor(globalSymbol) ); // name
alert( Symbol.keyFor(localSymbol) ); // undefined

系统Symbol

JavaScript内部有很多.系统Symbol,我们可以使用它们来微调对象的各个方面。

原文链接:zh.javascript.info/symbol

对象-原始值转换

我们已经掌握了方法和symbol的相关知识,可以开始学习原始值转换了。

  1. 所有的对象在布尔上下文中均为true。所以对于对象,不存在to-boolean转换,只有字符串和数值转换。
  2. 数值转换发生在对象相减或应用数学函数时。
  3. 至于字符串转换——通常发生在我们像alert(obj)这样输出一个对象和类似的上下文中。

ToPrimitive

下面是三个类型转换的变体,被称为"hint",在规范中有详细介绍:
"string"
对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如:

//输出
alert(obj);

//将对象作为属性键
anotherObj[obj] = 123;

"number" 对象到数字的转换,例如当我们进行数学运算时:

// 显示转换
let num = Number(obj);

// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 = date2;

// 小于、大于的比较
let greater = user1 > user2;

"default" 在少数情况下,当运算符“不确定”期望值的类型时。
例如,二元加法"+"可用于字符串连接,也可以用于数字相加,所以字符串和数字这两种类型都可以。因此,当二元假发得到对象类型的参数时,它将依据"default"hint来对其进行转换。
此外,如果对象被用于与字符串,数字或symbol进行==比较,这时到底应该进行哪种转换也不是很明确,因此,使用defaulthint。

除了一种情况(Date对象)之外,所有内建对象都以和"number"相同的方式实现"default"转换。

为了进行转换,JavaScript尝试查找并调用三个对象方法

  1. 调用obj[Symbol.toPrimitive](hint)——带有symbol键Symbol.toPrimitive(系统symbol)的方法,如果这个方法存在的话
  2. 否则,如果hint是"string"——尝试obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果hint是"number"或者"default"——尝试obj.valueOf()"obj.toString()",无论哪个存在。

Symbol.toPrimitive

有一个名为Symbol.toPrimitive的内建Symbol,它被用来给转换方法命名,像这样:

obj[Symbol.toPrimitive] = function(hint){
    // 返回一个原始值
    // hint = "string", "number""default"中的一个
}

例如:

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: defalut  1500

从代码中我们可以看到,根据转换的不同,user变成一个自描述字符串或者一个金额。单个方法user[Symbol.toPrimitive]处理了所有的转换情况。

toString/valueOf

方法toStringvalueOf来自上古时代。它们不是symbol,而是“常规的”字符串命名的方法。它们提供了一种可选的“老派的”实现转换的方法。
如果没有Symbol.toPrimtive,那么JavaScript将尝试找到它们,并且按照下面的顺序进行尝试:

  • 对于"string" hint,toString -> valueOf
  • 其他情况,valueOf -> toString

这些方法必须返回一个原始值。如果toStringvalueOf返回了一个对象,那么返回值会被忽略
默认情况下,普通对象具有toStringvalueOf方法:

  • toString方法返回一个字符串"[Object object]"
  • valueOf方法返回对象自身。

让我们实现一下这些方法。

let user = {
    name: "John",
    money: 1000,
    
    //对于hint = "string"
    toString(){
        return `{name: "${this.name}"}`;
    },
    
    // 对于hint = "number" 或 "default"
    valueOf(){
        return this.money;
    }
}

alert(user); // {name: "John"}
alert(+user); // 1000
alert(user + 500); // 1500

我们可以看到,执行的动作和前面使用Symbol.toPrimitive的那个例子相同。
通常我们希望有一个“全能”的地方来处理所有原始转换。在这种情况下,我们可以只实现toString

let user = {
    name: "John",
    
    toString(){
        return this.name;
    }
}

alert(user); // John
alert(user + 500); // John500

如果没有Symbol.toPrimitivevalueOf,toString将处理所有原始转换。

返回类型

关于所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回"hint"的原始值。 没有限制toStirng()是否返回字符串,或Symbol.toPrimitive方法是否为hint "number"返回数字。 唯一强制性的事情:这些方法必须返回一个原始值,而不是对象。

进一步的转换

如果我们将对象作为参数传递,则会出现两个阶段:

  1. 对象被转换为原始值(通过前面我们描述的规则)
  2. 如果生成的原始值的类型不正确,则继续进行转换。
/*
    1. 乘法 obj * 2 首先将对象转换为原始值(字符串 "2")
    2. 之后 "2" * 2 变为 2 * 2 (字符串被转换为数字)
*/
let obj = {
    toString(){
        return "2";
    }
}

alert(obj + 2); // 4

二元加法在同样的情况下会将其连接成字符串,因为它更愿意接受字符串:

let obj = {
    toString(){
        return "2";
    }
}

alert(obj + 2); // 22

在实践中,为了便于进行日志记录或调试,对于所有能够返回一种“可读性好”的对象的表达形式的转换,只实现以obj.toString()作为全能转换的方法就够了。

原文链接:zh.javascript.info/object-topr…

可选链 "?."

可选链

可选链?.是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。 举个例子:

let user = {}; // 一个没有“address”属性的user对象

alert( user.address.street ); // Uncaught TypeError: Cannot read property 'street' of undefined

JavaScript的工作原理就是这样的。因为user.addressundefined,尝试读取user.address.street会失败,并收到一个错误。
但是在很多实际场景中,我们更希望得到的是undefined(表示没有street属性)而不是一个错误。

这就是为什么可选链?.被加入到了JavaScript这门编程语言中。

如果可选链?.前面的部分是undefined或者null,它会停止运算并返回该部分。
上一个例子中,即使user不存在,使用user?.address来读取地址也没有问题:

let user = null;

alert(user?.address); // undefined
alert(user?.address?.street); //undefined

不要过度使用可选链
应该只将?.使用在一些东西可以不存在的地方
例如,如果根据我们的代码逻辑,user对象必须存在,但address是可选的,那么我们应该写成user.address?.street
?.前的变量必须已声明
如果未声明变量user,那么user?.anything会触发一个错误:

// Uncaught ReferenceError: user is not defined
user?.address;

可选链仅适用于已声明的变量

其他变体:?.() ?.[]

可选链?.不是一个运算符,而是一个特殊的语法结构,它还可以与函数和方括号一起使用。

let userAdmin = {
    admin(){
        alert("I'm admin");
    }
}

let userGuest = {};

userAdmin.admin?.(); // I'm admin
userGuest.admin?.(); // 啥都没有
let user1 = {
    firstName: "John"
};

let user2 = null;

let key = "firstName";

alert(user1?.[key]); // John
alert(user2?.[key]); // undefined

alert(user1?.[key]?.something?.not?.existing); // undefined

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

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

❗ 我们可以使用?.来安全地读取或删除,但不能写入
可选链?.不能用在赋值语句的左侧。

总结

  1. obj?.prop——如果obj存在则返回obj.prop,否则返回undefined.
  2. obj?.[prop]——如果obj,prop存在则返回obj[prop],否则返回undefined
  3. obj.method?.()——如果obj,obj.method存在则调用obj.method(),否则返回undefined

原文链接:zh.javascript.info/optional-ch…