每日一记
注:本章节较难,请选择性阅读
对象
使用{…}来创建对象。一个属性就是一个键值对(“key: value”),其中键是一个字符串(也叫做属性名),值可以是任何值。
构造对象方法:
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
读取文件的属性:
alert( user.name ); // John
alert( user.age ); // 30
移除属性:
delete user.age; // 移除age这个key
方括号:
let user = {};
// 设置
user["likes birds"] = true; // 单引号或双引号都可以
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];
属性值简写:
name, 与 name: name 相同
属性名称限制:
类型会被自动地转换为字符串
但__proto__属性。不能将它设置为一个非对象的值
属性存在性测试:
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性
alert( "age" in user ); // true 存在
for..in 循环
for (key in object) {
// 对此对象属性中的每个键执行的代码
}
遍历一个对象时,整数属性会被进行从小到大排序
但是 “+49” 和 “1.2” 不行,那它们就按照创建时的顺序来排序
对象引用和复制
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”。
对象的引用:
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // 通过 "admin" 引用来修改
alert(user.name); // 'Pete',修改能通过 "user" 引用看到
仅当两个对象为同一对象时,两者才相等。
两个独立的对象则并不相等,即使它们看起来很像(都为空)。
对象的复制
通过遍历已有对象的属性,并在原始类型值的层面复制它们,以实现对已有对象结构的复制。
let user = {
name: "John",
};
let clone = {}; // 新的空对象
for (let key in user) { // 将 user 中所有的属性拷贝到其中
clone[key] = user[key]; // 现在 clone 是带有相同内容的完全独立的对象
}
clone.name = "Pete"; // 改变了其中的数据
alert( user.name ); // 原来的对象中的 name 属性依然是 John
【也可以使用 Object.assign 方法】
Object.assign(dest, [src1, src2, src3...])
1.dest 是指目标对象
2.src1, ..., srcN是源对象(需传递的参数)
3.将所有源对象的属性拷贝到目标对象 dest 中
4.调用结果返回 dest
5.如果被拷贝的属性的属性名已经存在,那么它会被覆盖
深层克隆
let 克隆生成的对象 = Object.assign({}, 克隆的对象);
或者使用“深拷贝”函数,例如 _.cloneDeep(obj)
垃圾回收
1.可达性
如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的
垃圾回收器会删除掉那些已经不可达的(没有外部对其的引用)
2.内部算法(mark-and-sweep):
垃圾收集器遍历所有的根,并“标记”(记住)它们和他们的引用。
没有被标记的对象都会被删除。
3.优化建议:
分代收集/增量收集/闲时收集
进阶了解
对象方法
存储在对象属性中的函数被称为“方法”
使用函数表达式创建一个函数,并将其指定给对象的属性,随后调用它。就叫做得到了 user 对象的 sayHi 方法。
this
访问对象中存储的信息
this可以用于任何函数,它取决于代码上下文
严格模式下没有对象的情况下调用:this==undefined但尝试访问this.name将会报错
this的值是在调用时计算出来的,它的值取决于在“点符号前”的是什么对象。
箭头函数没有自己的this。若在箭头函数中引用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(使用点符号或方括号语法来访问这个方法,都没有关系。)
重点误区,必须记住
构造器和操作符 “new”
构造函数
命名以大写字母开头。
只能由 “new” 操作符来执行
function User(name) { 1.一个新的空对象被创建并分配给 this。
this.name = name; 2.函数体执行。通常它会修改 this,为其添加新的属性。
this.isAdmin = false; 3.返回 this 的值。
}
任何函数(除了箭头函数,它没有自己的 this)都可以用作构造器
new function() { … }
如果有许多行用于创建单个复杂对象的代码,可以将它们封装在一个立即调用的构造函数中
// 创建一个函数并立即使用 new 调用它
let user = new function() {
this.name = "John";
this.isAdmin = false;
// ……用于用户创建的其他代码
// 也许是复杂的逻辑和语句
// 局部变量等
};
这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用。
因此,这个技巧旨在封装构建单个对象的代码,而无需将来重用。
构造器模式测试,无需重视
可选链
语法有三种形式:
obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined。
obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined。
obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined。
总结?. 检查左边部分是否为 null/undefined,如果不是则继续运算。
symbol 类型
“symbol” 值表示唯一的标识符。
使用 Symbol() 来创建这种类型的值
可以给 symbol 一个描述(也称为 symbol 名) //let id = Symbol(“id”);
symbol 保证是唯一的。即使我们创建了许多具有相同描述的 symbol,它们的值也是不同。symbol 不会被自动转换为字符串
真的想显示一个 symbol,我们需要
在它上面调用 .toString()
let id = Symbol("id");
alert(id.toString()); //Symbol(id)
或者获取 symbol.description 属性,只显示描述
let id = Symbol("id");
alert(id.description); // id
主要的使用场景:
1.“隐藏” 对象属性。
如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 symbol 并使用它作为属性的键。
2.JavaScript 使用了许多系统 symbol,这些 symbol 可以作为 Symbol.* 访问。我们可以使用它们来改变一些内建行为。
3.symbol 在 for…in 中会被跳过
4.Object.keys(user) 也会忽略它们
5.相反,Object.assign 会同时复制字符串和 symbol 属性
对象 —— 原始值转换
JavaScript 不允许自定义运算符对对象的处理方式
此类运算的情况下,对象会被自动转换为原始值,然后对这些原始值进行运算,并得到运算结果(也是一个原始值)
转换规则
1.没有转换为布尔值,所有的对象在布尔上下文(context)中均为 true
2.数字转换发生在对象相减或应用数学函数时
3.字符串转换通常发生在像 alert(obj) 这样输出一个对象和类似的上下文中
hint
“string”(对于 alert 和其他需要字符串的操作)
“number”(对于数学运算)
“default”(少数运算符,通常对象以和 “number” 相同的方式实现 “default” 转换)
Symbol.toPrimitive
用来给转换方法命名
toString/valueOf
对于 “string” hint:调用 toString 方法,如果它不存在,则调用 valueOf 方法(因此,对于字符串转换,优先调用 toString)。
对于其他 hint:调用 valueOf 方法,如果它不存在,则调用 toString 方法(因此,对于数学运算,优先调用 valueOf 方法)。
转换可以返回任何原始类型
它们不一定会返回 “hint” 的原始值
唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象。
进一步的转换
如果我们将对象作为参数传递,则会出现两个运算阶段:
1.对象被转换为原始值(通过前面我们描述的规则)。
2.如果还需要进一步计算,则生成的原始值会被进一步转换。
总结:
首先调用 obj[Symbol.toPrimitive](hint) 如果这个方法存在,
否则,如果 hint 是 "string"
尝试调用 obj.toString() 或 obj.valueOf(),无论哪个存在。
否则,如果 hint 是 "number" 或者 "default"
尝试调用 obj.valueOf() 或 obj.toString(),无论哪个存在。
在实际使用中,通常只实现 obj.toString() 作为字符串转换的“全能”方法就足够了,该方法应该返回对象的“人类可读”表示,用于日志记录或调试。