现代js教程摘录(对象)

时间:2021-1-8 作者:admin

本博客内容均摘录自现代js教程,连接如下

https://zh.javascript.info

本文知识摘录一些自己认为需要知道的重点知识,并不是完整的js学习文章,由于篇幅很长,所以分多次发布

Object(对象):基础知识

我们可以用下面两种语法中的任一种来创建一个空的对象(“空柜子”):

let user = new Object(); // “构造函数” 的语法
let user = {};  // “字面量” 的语法

我们可以用 delete 操作符移除属性:

delete user.age;

我们也可以用多字词语来作为属性名,但必须给它们加上引号:

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // 多词属性名必须加引号
};

列表中的最后一个属性应以逗号结尾:

let user = {
  name: "John",
  age: 30,
}

这叫做尾随(trailing)或悬挂(hanging)逗号。这样便于我们添加、删除和移动属性,因为所有的行都是相似的。

使用 const 声明的对象是可以被修改的

请注意:用 const 声明的对象 被修改。

例如:

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

(*) 行似乎会触发一个错误,但实际并没有。const 声明仅固定了 user 的值,而不是值(该对象)里面的内容。

仅当我们尝试将 user=... 作为一个整体进行赋值时,const 会抛出错误。

对于多词属性,点操作就不能用了:

// 这将提示有语法错误
user.likes birds = true

JavaScript 理解不了。它认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。

点符号要求 key 是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 $_)。

有另一种方法,就是使用方括号,可用于任何字符串:

let user = {};

// 设置
user["likes birds"] = true;

// 读取
alert(user["likes birds"]); // true

// 删除
delete user["likes birds"];

现在一切都可行了。请注意方括号中的字符串要放在引号中,单引号或双引号都可以。

方括号同样提供了一种可以通过任意表达式来获取属性名的方法 —— 跟语义上的字符串不同 —— 比如像类似于下面的变量:

let key = "likes birds";

// 跟 user["likes birds"] = true; 一样
user[key] = true;

在这里,变量 key 可以是程序运行时计算得到的,也可以是根据用户的输入得到的。然后我们可以用它来访问属性。这给了我们很大的灵活性。

例如:

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// 访问变量
alert( user[key] ); // John(如果输入 "name")

点符号不能以类似的方式使用:

let user = {
  name: "John",
  age: 30
};

let key = "name";
alert( user.key ) // undefined

当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性

例如:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
};

alert( bag.apple ); // 5 如果 fruit="apple"

计算属性的含义很简单:[fruit] 含义是属性名应该从 fruit 变量中获取。

所以,如果一个用户输入 "apple"bag 将变为 {apple: 5}

本质上,这跟下面的语法效果相同:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

……但是看起来更好。

我们可以在方括号中使用更复杂的表达式:

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

方括号比点符号更强大。它允许任何属性名和变量,但写起来也更加麻烦。

所以,大部分时间里,当属性名是已知且简单的时候,就使用点符号。如果我们需要一些更复杂的内容,那么就用方括号。

我们已经知道,变量名不能是编程语言的某个保留字,如 “for”、“let”、“return” 等……

但对象的属性名并不受此限制:

// 这些属性都没问题
let obj = {
  for: 1,
  let: 2,
  return: 3
};

alert( obj.for + obj.let + obj.return );  // 6

简而言之,属性命名没有限制。属性名可以是任何字符串或者 symbol(一种特殊的标志符类型,将在后面介绍)。

其他类型会被自动地转换为字符串。

例如,当数字 0 被用作对象的属性的键时,会被转换为字符串 "0"

let obj = {
  0: "test" // 等同于 "0": "test"
};

// 都会输出相同的属性(数字 0 被转为字符串 "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (相同的属性)

这里有个小陷阱:一个名为 __proto__ 的属性。我们不能将它设置为一个非对象的值:

let obj = {};
obj.__proto__ = 5; // 分配一个数字
alert(obj.__proto__); // [object Object] — 值为对象,与预期结果不同

我们从代码中可以看出来,把它赋值为 5 的操作被忽略了。

相比于其他语言,JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!

读取不存在的属性只会得到 undefined。所以我们可以很容易地判断一个属性是否存在:

let user = {};

alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性

相比于其他语言,JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!

读取不存在的属性只会得到 undefined。所以我们可以很容易地判断一个属性是否存在:

let user = {};

alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性

这里还有一个特别的,检查属性是否存在的操作符 "in"

语法是:

"key" in object

例如:

let user = { name: "John", age: 30 };

alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。

请注意,in 的左边必须是 属性名。通常是一个带引号的字符串。

如果我们省略引号,就意味着左边是一个变量,它应该包含要判断的实际属性名。例如:

let user = { age: 30 };

let key = "age";
alert( key in user ); // true,属性 "age" 存在

为何会有 in 运算符呢?与 undefined 进行比较来判断还不够吗?

确实,大部分情况下与 undefined 进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但 in 运算符的判断结果仍是对的。

那就是属性存在,但存储的值是 undefined 的时候:

let obj = {
  test: undefined
};

alert( obj.test ); // 显示 undefined,所以属性不存在?

alert( "test" in obj ); // true,属性存在!

在上面的代码中,属性 obj.test 事实上是存在的,所以 in 操作符检查通过。

这种情况很少发生,因为通常情况下不应该给对象赋值 undefined。我们通常会用 null 来表示未知的或者空的值。因此,in 运算符是代码中的特殊来宾。

为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in。这跟我们在前面学到的 for(;;) 循环是完全不一样的东西。

语法:

for (key in object) {
  // 对此对象属性中的每个键执行的代码
}

例如,让我们列出 user 所有的属性:

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // 属性键的值
  alert( user[key] ); // John, 30, true
}

注意,所有的 “for” 结构体都允许我们在循环中定义变量,像这里的 let key

同样,我们可以用其他属性名来替代 key。例如 "for(let prop in obj)" 也很常用。

对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?

简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。详情如下:

例如,让我们考虑一个带有电话号码的对象:

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}

对象可用于面向用户的建议选项列表。如果我们的网站主要面向德国观众,那么我们可能希望 49 排在第一。

但如果我们执行代码,会看到完全不同的现象:

  • USA (1) 排在了最前面
  • 然后是 Switzerland (41) 及其它。

因为这些电话号码是整数,所以它们以升序排列。所以我们看到的是 1, 41, 44, 49

整数属性?那是什么?

这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。

所以,“49” 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。但是 “+49” 和 “1.2” 就不行了:

// Math.trunc 是内置的去除小数部分的方法。
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性

……此外,如果属性名不是整数,那它们就按照创建时的顺序来排序,例如:

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // 增加一个

// 非整数属性是按照创建的顺序来排列的
for (let prop in user) {
  alert( prop ); // name, surname, age
}

所以,为了解决电话号码的问题,我们可以使用非整数属性名来 欺骗 程序。只需要给每个键名加一个加号 "+" 前缀就行了。

像这样:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for (let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

现在跟预想的一样了。

写一个 isEmpty(obj) 函数,当对象没有属性的时候返回 true,否则返回 false

应该像这样:

let schedule = {};

alert( isEmpty(schedule) ); // true

schedule["8:30"] = "get up";

alert( isEmpty(schedule) ); // false

打开带有测试的沙箱。

解决方案

只需要遍历这个对象,如果对象存在任何属性则 return false

function isEmpty(obj) {
  for (let key in obj) {
    // 如果进到循环里面,说明有属性。
    return false;
  }
  return true;
}

使用沙箱的测试功能打开解决方案。

创建一个 multiplyNumeric(obj) 函数,把 obj 所有的数值属性都乘以 2

例如:

// 在调用之前
let menu = {
  width: 200,
  height: 300,
  title: "My menu"
};

multiplyNumeric(menu);

// 调用函数之后
menu = {
  width: 400,
  height: 600,
  title: "My menu"
};

注意 multiplyNumeric 函数不需要返回任何值,它应该就地修改对象。

P.S. 用 typeof 检查值类型。

打开带有测试的沙箱。

解决方案

function multiplyNumeric(obj) {
  for (let key in obj) {
    if (typeof obj[key] == 'number') {
      obj[key] *= 2;
    }
  }
}

使用沙箱的测试功能打开解决方案。

对象与原始类型其中一个基本的区别是:对象“通过引用的形式”被存储和拷贝。

原始类型值:字符串,数字,布尔值 —— 被“作为整体”赋值/拷贝。

例如:

let message = "Hello!";
let phrase = message;

结果我们就有了两个独立的变量,每个都存储着字符串 "Hello!"

对象不是这样的。

变量存储的不是对象自身,而是该对象的“内存地址”,换句话说就是一个对该对象的“引用”。

下面是这个对象的示意图:

let user = {
  name: "John"
};

这里,该对象被存储在内存中的某处。而变量 user 保存的是对此处的“引用”。

当一个对象变量被拷贝 —— 引用则被拷贝,而该对象并没有被复制。

例如:

let user = { name: "John" };

let admin = user; // 拷贝引用

现在我们有了两个变量,它们保存的都是对同一个对象的引用:

我们可以用任何变量来访问该对象并修改它的内容:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // 通过 "admin" 引用来修改

alert(user.name); // 'Pete',修改能通过 "user" 引用看到

上面的例子说明这里只有一个对象。就像我们有个带两把钥匙的锁柜,并使用其中一把钥匙(admin)来打开它。那么,我们如果之后用另外一把钥匙(user),就也能看到所作的改变。

对于对象来说,普通相等 == 和严格相等 === 是两个作用结果完全一样的运算符。

仅当两个对象为同一对象时,两者才相等。

这里两个变量都引用同一个对象,所以它们相等:

let a = {};
let b = a; // 拷贝引用

alert( a == b ); // true,都引用同一对象
alert( a === b ); // true

而这里两个独立的对象则并不相等,即使它们都为空:

let a = {};
let b = {}; // 两个独立的对象

alert( a == b ); // false

对于类似 obj1 > obj2 的比较,或者跟一个原始类型值的比较 obj == 5,对象都会被转换为原始值。我们很快就会学到对象是如何转换的,但是说实话,类似的比较很少出现,通常是在编程错误的时候才会出现这种情况。

那么,拷贝一个对象变量会又创建一个对相同对象的引用。

但是,如果我们想要复制一个对象,那该怎么做呢?创建一个独立的拷贝,克隆?

这也是可行的,但稍微有点困难,因为 JavaScript 没有提供对此操作的内建的方法。实际上,也很少需要这样做。通过引用进行拷贝在大多数情况下已经很好了。

但是,如果我们真的想要这样做,那么就需要创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。

就像这样:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 将 user 中所有的属性拷贝到其中
for (let key in user) {
  clone[key] = user[key];
}

// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据

alert( user.name ); // 原来的对象中的 name 属性依然是 John

我们也可以使用 Object.assign 方法来达成同样的效果。

语法是:

Object.assign(dest, [src1, src2, src3...])
  • 第一个参数 dest 是指目标对象。
  • 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。
  • 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
  • 调用结果返回 dest

例如,我们可以用它来合并多个对象:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);

// 现在 user = { name: "John", canView: true, canEdit: true }

如果被拷贝的属性的属性名已经存在,那么它会被覆盖:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // 现在 user = { name: "Pete" }

我们也可以用 Object.assign 代替 for..in 循环来进行简单克隆:

let user = {
  name: "John",
  age: 30
};

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

它将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。

到现在为止,我们都假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?

例如:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 cloneuser 会共用一个 sizes:

就像这样:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

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

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 分享同一个 sizes
user.sizes.width++;       // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个看到变更的结果

为了解决此问题,我们应该使用会检查每个 user[key] 的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。

这里有一个标准的深拷贝算法,它不仅能处理上面的例子,还能应对更多复杂的情况,它被称为 结构化拷贝算法

我们可以用递归来实现。或者不自己造轮子,使用现成的实现,例如 JavaScript 库 lodash 中的 _.cloneDeep(obj)

// 深拷贝解决方案

function DeepCopy(source){
var sourceCopy = source instanceof Array ? [] : {};
for (var item in source) {
sourceCopy[item] = typeof source[item] 
=== 'object' ? DeepCopy(source[item]) : source[item];
}
return sourceCopy;
}
垃圾回收机制

zh.javascript.info/garbage-col…

this

通常,对象方法需要访问对象中存储的信息才能完成其工作。

例如,user.sayHi() 中的代码可能需要用到 user 的 name 属性。

为了访问该对象,方法中可以使用 this 关键字。

this 的值就是在点之前的这个对象,即调用该方法的对象。

举个例子:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" 指的是“当前的对象”
    alert(this.name);
  }

};

user.sayHi(); // John

在这里 user.sayHi() 执行过程中,this 的值是 user

技术上讲,也可以在不使用 this 的情况下,通过外部变量名来引用它:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // "user" 替代 "this"
  }

};

……但这样的代码是不可靠的。如果我们决定将 user 复制给另一个变量,例如 admin = user,并赋另外的值给 user,那么它将访问到错误的对象。

下面这个示例证实了这一点:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // 导致错误
  }

};


let admin = user;
user = null; // 重写让其更明显

admin.sayHi(); // 噢哟!在 sayHi() 使用了旧的 name 属性!报错!

如果我们在 alert 中以 this.name 替换 user.name,那么代码就会正常运行。

在 JavaScript 中,this 关键字与其他大多数编程语言中的不同。JavaScript 中的 this 可以用于任何函数。

下面这样的代码没有语法错误:

function sayHi() {
  alert( this.name );
}

this 的值是在代码运行时计算出来的,它取决于代码上下文。

例如,这里相同的函数被分配给两个不同的对象,在调用中有着不同的 “this” 值:

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)

这个规则很简单:如果 obj.f() 被调用了,则 thisf 函数调用期间是 obj。所以在上面的例子中 this 先是 user,之后是 admin

在没有对象的情况下调用:this == undefined

我们甚至可以在没有对象的情况下调用函数:

function sayHi() {
  alert(this);
}

sayHi(); // undefined

在这种情况下,严格模式下的 this 值为 undefined。如果我们尝试访问 this.name,将会报错。

在非严格模式的情况下,this 将会是 全局对象(浏览器中的 window,我们稍后会在 全局对象 一章中学习它)。这是一个历史行为,"use strict" 已经将其修复了。

通常这种调用是程序出错了。如果在一个函数内部有 this,那么通常意味着它是在对象上下文环境中被调用的。

解除 this 绑定的后果

如果你经常使用其他的编程语言,那么你可能已经习惯了“绑定 this”的概念,即在对象中定义的方法总是有指向该对象的 this

在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。

在运行时对 this 求值的这个概念既有优点也有缺点。一方面,函数可以被重用于不同的对象。另一方面,更大的灵活性造成了更大的出错的可能。

这里我们的立场并不是要评判编程语言的这个设计是好是坏。而是要了解怎样使用它,如何趋利避害。

箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 thisthis 值取决于外部“正常的”函数。

举个例子,这里的 arrow() 使用的 this 来自于外部的 user.sayHi() 方法:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

这是箭头函数的一个特性,当我们并不想要一个独立的 this,反而想从外部上下文中获取时,它很有用。在后面的 深入理解箭头函数 一章中,我们将深入介绍箭头函数。

构造器和new

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行。

例如:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

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

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

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

function User(name) {
  // this = {};(隐式创建)

  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隐式返回)
}

所以 new User("Jack") 的结果是相同的对象:

let user = {
  name: "Jack",
  isAdmin: false
};

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

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

让我们再强调一遍 —— 从技术上讲,任何函数都可以用作构造器。即:任何函数都可以通过 new 来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new 来运行。

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

但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略。

换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this

例如,这里 return 通过返回一个对象覆盖 this

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- 返回这个对象
}

alert( new BigUser().name );  // Godzilla,得到了那个对象

这里有一个 return 为空的例子(或者我们可以在它之后放置一个原始类型,没有什么影响):

function SmallUser() {

  this.name = "John";

  return; // <-- 返回 this
}

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

通常构造器没有 return 语句。这里我们主要为了完整性而提及返回对象的特殊行为。

省略括号

顺便说一下,如果没有参数,我们可以省略 new 后的括号:

let user = new User; // <-- 没有参数
// 等同于
let user = new User();

这里省略括号不被认为是一种“好风格”,但是规范允许使用该语法。

可选链 “?.”

点击查看原文

Symbol 类型

根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。

到目前为止,我们只见过字符串。现在我们来看看 Symbol 能给我们带来什么好处。

Symbol

“Symbol” 值表示唯一的标识符。

可以使用 Symbol() 来创建这种类型的值:

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

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

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

Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。

例如,这里有两个描述相同的 Symbol —— 它们不相等:

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Symbol 不会被自动转换为字符串

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

例如,这个 alert 将会提示出错:

let id = Symbol("id");
alert(id); // 类型错误:无法将 Symbol 值转换为字符串。

这是一种防止混乱的“语言保护”,因为字符串和 Symbol 有本质上的不同,不应该意外地将它们转换成另一个。

如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString(),如下所示:

let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了

或者获取 symbol.description 属性,只显示描述(description):

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

Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。

例如,如果我们使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符。

我们可以给它们使用 Symbol 键:

let user = { // 属于另一个代码
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据

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

因为 user 对象属于其他的代码,那些代码也会使用这个对象,所以我们不应该在它上面直接添加任何字段,这样很不安全。但是你添加的 Symbol 属性不会被意外访问到,第三方代码根本不会看到它,所以使用 Symbol 基本上不会有问题。

另外,假设另一个脚本希望在 user 中有自己的标识符,以实现自己的目的。这可能是另一个 JavaScript 库,因此脚本之间完全不了解彼此。

然后该脚本可以创建自己的 Symbol("id"),像这样:

// ...
let id = Symbol("id");

user[id] = "Their id value";

我们的标识符和它们的标识符之间不会有冲突,因为 Symbol 总是不同的,即使它们有相同的名字。

……但如果我们处于同样的目的,使用字符串 "id" 而不是用 symbol,那么 就会 出现冲突:

let user = { name: "John" };

// 我们的脚本使用了 "id" 属性。
user.id = "Our id value";

// ……另一个脚本也想将 "id" 用于它的目的……

user.id = "Their id value"
// 砰!无意中被另一个脚本重写了 id!
字面量中的 Symbol

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

就像这样:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // 而不是 "id":123
};

这是因为我们需要变量 id 的值作为键,而不是字符串 “id”。

Symbol 在 for…in 中会被跳过

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

例如:

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

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

// 使用 Symbol 任务直接访问
alert( "Direct: " + user[id] );

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

相反,Object.assign 会同时复制字符串和 symbol 属性:

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

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

alert( clone[id] ); // 123

这里并不矛盾,就是这样设计的。这里的想法是当我们克隆或者合并一个 object 时,通常希望 所有 属性被复制(包括像 id 这样的 Symbol)。

全局 symbol

正如我们所看到的,通常所有的 Symbol 都是不同的,即使它们有相同的名字。但有时我们想要名字相同的 Symbol 具有相同的实体。例如,应用程序的不同部分想要访问的 Symbol "id" 指的是完全相同的属性。

为了实现这一点,这里有一个 全局 Symbol 注册表。我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名字的 Symbol 时,返回的都是相同的 Symbol。

要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key)

该调用会检查全局注册表,如果有一个描述为 key 的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)),并通过给定的 key 将其存储在注册表中。

例如:

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

// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");

// 相同的 Symbol
alert( id === idAgain ); // true

注册表内的 Symbol 被称为 全局 Symbol。如果我们想要一个应用程序范围内的 Symbol,可以在代码中随处访问 —— 这就是它们的用途。

symbol总结

Symbol 是唯一标识符的基本类型

Symbol 是使用带有可选描述(name)的 Symbol() 调用创建的。

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

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个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。