构造器和操作符
构造函数
使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。
构造函数在技术上是常规函数,不过它有两个规定:
- 它们的命名以大写字母开头。
- 它们只能由
"new"操作符实现。
function User(name){
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack:);
当一个函数被使用new操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this - 函数体执行。通常它会修改
this,为其添加新的属性。 - 返回
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语句,那么:
- 如果
return返回的是一个对象,则返回这个对象 - 如果
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的相关知识,可以开始学习原始值转换了。
- 所有的对象在布尔上下文中均为
true。所以对于对象,不存在to-boolean转换,只有字符串和数值转换。 - 数值转换发生在对象相减或应用数学函数时。
- 至于字符串转换——通常发生在我们像
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尝试查找并调用三个对象方法
- 调用
obj[Symbol.toPrimitive](hint)——带有symbol键Symbol.toPrimitive(系统symbol)的方法,如果这个方法存在的话 - 否则,如果hint是
"string"——尝试obj.toString()和obj.valueOf(),无论哪个存在。 - 否则,如果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
方法toString和valueOf来自上古时代。它们不是symbol,而是“常规的”字符串命名的方法。它们提供了一种可选的“老派的”实现转换的方法。
如果没有Symbol.toPrimtive,那么JavaScript将尝试找到它们,并且按照下面的顺序进行尝试:
- 对于"string" hint,
toString -> valueOf - 其他情况,
valueOf -> toString
这些方法必须返回一个原始值。如果toString或valueOf返回了一个对象,那么返回值会被忽略
默认情况下,普通对象具有toString和valueOf方法:
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.toPrimitive和valueOf,toString将处理所有原始转换。
返回类型
关于所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回"hint"的原始值。
没有限制toStirng()是否返回字符串,或Symbol.toPrimitive方法是否为hint "number"返回数字。
唯一强制性的事情:这些方法必须返回一个原始值,而不是对象。
进一步的转换
如果我们将对象作为参数传递,则会出现两个阶段:
- 对象被转换为原始值(通过前面我们描述的规则)
- 如果生成的原始值的类型不正确,则继续进行转换。
/*
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.address为undefined,尝试读取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
❗ 我们可以使用
?.来安全地读取或删除,但不能写入
可选链?.不能用在赋值语句的左侧。
总结
obj?.prop——如果obj存在则返回obj.prop,否则返回undefined.obj?.[prop]——如果obj,prop存在则返回obj[prop],否则返回undefined。obj.method?.()——如果obj,obj.method存在则调用obj.method(),否则返回undefined。