前言
第一章 -
第二章 -
第三章 - 语言基础
第八章 - 对象、类与面向对象编程
对象的创建方式:
let objA = new Object();
objA.name = "A";
// 对象字面量
let objB = {
name: "B"
};
console.log(objA, objB); // {name: 'A'} {name: 'B'}
对象的属性有两种:数据属性、访问器属性。数据属性有 4 个特性描述其行为(为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来):
[[Configurable]]
,是否可以通过delete
删除,默认为true
[[Enumerable]]
,是否可以通过for-in
循环,默认为true
[[Writable]]
,是否可以修改,默认为true
[[Value]]
,实际值,默认为undefined
可以通过 Object.defineProperty
或 Object.defineProperties
定义属性及其特性,也可以通过 Object.getOwnPropertyDescriptor
或 Object.getOwnPropertyDescriptors
读取属性的特性。
let obj = {};
Object.defineProperty(
obj,
"name", // 针对 name 属性
{
configurable: false, // 不可删除
enumerable: true,
writable: false, // 不可修改
value: "kingcos" // 实际值
}
);
// Uncaught TypeError: Cannot redefine property: name
// Object.defineProperty(
// obj,
// "name", // 针对 name 属性
// {
// configurable: true // 将 false 改为 true 将抛出异常
// // 其他属性默认为 false
// }
// );
console.log(obj.name); // kingcos
obj.name = "javascript"; // 严格模式时,会抛出错误
delete obj.name; // 同上
console.log(obj.name); // kingcos
// {value: 'kingcos', writable: false, enumerable: true, configurable: false}
console.log(Object.getOwnPropertyDescriptor(obj, "name"));
// {name: {value: 'kingcos', writable: false, enumerable: true, configurable: false}}
console.log(Object.getOwnPropertyDescriptors(obj));
访问器属性不包含数据值,但包含 getter & setter;其也有 4 个特性描述其行为。
[[Configurable]]
,是否可以通过delete
删除,默认为true
[[Enumerable]]
,是否可以通过for-in
循环,默认为true
[[Get]]
,getter,默认为undefined
[[Set]]
,setter,默认为undefined
let obj = {
name: "obj",
age_: 27
};
Object.defineProperties(obj, {
age: {
get() {
console.log("getter");
return this.age_;
},
set(newValue) {
console.log("setter");
if (newValue > 0) {
this.age_ = newValue;
}
}
}
});
console.log(obj.age); // getter 27
obj.age = 18; // setter
console.log(obj.age); // getter 18
Object.assign(target, source)
合并(Merge)对象,又称混入(Mixin)。另外,需要注意的是:
- 对源对象所执行的是浅复制(即引用)
- 对于多个源对象中的相同属性,使用最后一个复制的值
getter
和setter
不会被合并,仅合并静态值- 合并过程中若抛出错误,将可能只有部分属性得到合并
// eg.1
let tgt = {};
let src = { name: "kingcos" };
// Object.assign 将 src 混入 tgt,并返回 tgt
let res = Object.assign(tgt, src);
console.log(tgt === res); // true
console.log(tgt === src); // false
// {name: 'kingcos'} {name: 'kingcos'} {name: 'kingcos'}
console.log(tgt, src, res);
// eg.2
let tgt2 = {
set a(newValue) {
console.log(`setter ${newValue}`);
}
};
let src2 = {
get a() {
console.log(`getter`);
return "kingcos.me";
}
}
// getter // 调用 src2 getter
// setter kingcos.me // 调用 tgt2 setter,并传入上一步的值
Object.assign(tgt2, src2);
// {}
console.log(tgt2); // tgt2 的 setter 没有实际赋值,因此这里仍为 {}
// eg.3
let tgt3 = {};
let src3 = { name: "kingcos", obj: {}, get a() { return 1; } };
let res3 = Object.assign(tgt3, src3, { name: "kingcos.me" });
// 以最后一个对象的 name 值为准
// {name: 'kingcos.me', obj: {}} {name: 'kingcos', obj: {}} {name: 'kingcos.me', obj: {}}
console.log(tgt3, src3, res3);
src3.obj.name = "obj";
// 浅复制
// true
console.log(tgt3.obj === src3.obj);
// {name: 'kingcos.me', obj: {name: "obj"}} {name: 'kingcos', obj: {name: "obj"}} {name: 'kingcos.me', obj: {name: "obj"}}
console.log(tgt3, src3, res3);
tgt3 = {}
src3 = {
name: "kingcos",
age_: 27,
get age() {
console.log("getter age");
return this.age_;
},
get a() {
throw new Error();
},
obj: {}
}
try {
// getter age
Object.assign(tgt3, src3);
} catch(e) {
console.log(e); // Error,中断赋值
}
console.log(tgt3); // {name: 'kingcos', age_: 27, age: 27}
tgt3.age += 1;
console.log(tgt3); // {name: 'kingcos', age_: 27, age: 28}
对象判等:
==
:比较两个操作数是否相等,若类型不同,会尝试转换为相同的类型再进行比较===
:比较两个操作数是否相等,并且要求类型也相同Object.is()
:ES6 新增方法
console.log(true === 1); // false,类型不同
console.log({} === {}); // false,对象地址不同
console.log("2" === 2); // false,类型不同
// +0 与 -0 均表示数值 0,但在计算机内部的表示方式略有不同,是两个不同的值
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 除以 +0 相当于除以一个很小的正数,而除以 -0 相当于除以一个很小的负数
// 因此它们的商的正负号和除数的正负号相反
console.log(1 / +0); // Infinity
console.log(1 / -0); // -Infinity
console.log(Object.is(+0, -0)); // false
console.log(Object.is(0, -0)); // true
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)); // true
// 检查多个值是否相等
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
ES6 中增强的对象语法:
// 1. 属性值简写
let name = "kingcos";
let obj = {
name
};
console.log(obj); // {name: 'kingcos'}
function makeObj(name) {
return { name };
}
// 如果使用 Google Closure 编译器压缩,那么函数参数会被缩短,而属性名不变:
// function makeObj(a) {
// return { name: a };
// }
let obj2 = makeObj("kingcos.me");
console.log(obj2.name); // 这里编译器仍会保留初始的 name 标识符
// 2. 可计算属性
let obj3 = {};
let keyVar = "name";
obj3[keyVar] = "kingcos";
console.log(obj3); // {name: 'kingcos'}
obj3 = {
[keyVar]: "kingcos.me"
};
// 注意:可计算属性表达式中抛出任何错误都会中断对象创建
console.log(obj3); // {name: 'kingcos.me'}
// 3. 简写方法名
let obj4 = {
printName: function (name) {
console.log(`name: ${name}`)
},
printNameNew(name) { // 简写版本
console.log(`name: ${name}`)
},
name_: "kingcos",
get name() { // 简化写法(非简化即 Object.defineProperty)
return this.name_;
},
set name(name) {
this.name_ = name;
},
[keyVar + "Output"](name) {
console.log(`name: ${name}`)
}
};
obj4.printName("kingcos"); // name: kingcos
obj4.printNameNew("kingcos.me"); // name: kingcos.me
console.log(obj4.name); // kingcos
obj4.nameOutput("kingcos"); // name: kingcos
// 4. 对象解构(Destructure)
let obj5 = {
name: "kingcos",
gender: "Male"
};
let { name: objName, age: objAge } = obj5;
let { name, age = 18 } = obj5;
console.log(objName, objAge); // kingcos undefined
console.log(name, age); // kingcos, 18
// 解构在内部使用函数 `ToObject()`(不能在运行时环境中直接访问)把源数据结构转换为对象
// 如下将字符串被当作对象
let { length } = "kingcos";
console.log(length); // 7
// 也因此,null 与 undefined 不是真正的对象,所以也不能被解构
// Uncaught TypeError: Cannot destructure property '_' of 'null' as it is null.
// let { _ } = null;
// Uncaught TypeError: Cannot destructure property '_' of 'undefined' as it is undefined.
// let { _ } = undefined;
// 已声明的变量若使用解构赋值,则需要使用 `()`
let nameVar;
({ name: nameVar } = obj5);
console.log(nameVar); // kingcos
let obj6 = {
name: "kingcos",
age: 27,
job: {
title: "SDE"
}
};
let obj6_copy = {};
({ name: obj6_copy.name, job: obj6_copy.job } = obj6);
console.log(obj6_copy.name); // kingcos
// 对象的赋值是针对引用的赋值,即浅复制
obj6.job.title = "RD";
console.log(obj6_copy.job.title); // RD
// 嵌套也可支持
let { job: { title } } = obj6;
console.log(title); // RD
// 外层属性未定义时将不能使用嵌套解构
// TypeError: Cannot read properties of undefined (reading 'bar')
// ({ foo: { bar: bar } } = obj6);
obj6_copy = {};
try {
({ name: obj6_copy.name, job: { title: obj6_copy.work.title }, age: obj6_copy.age} = obj6);
} catch(e) {
// TypeError: Cannot set properties of undefined (setting 'title')
console.log(e);
}
// 赋值出错时,将可能仅部分解构
console.log(obj6_copy); // {name: 'kingcos'}
// 参数上下文匹配
let obj7 = { name: "kingcos", age: 27 };
function func1({name, age}) {
// 对参数解构赋值不影响 arguments 对象
console.log(arguments); // Arguments [{name: 'kingcos', age: 27}, callee: (...), Symbol(Symbol.iterator): ƒ]
console.log(name, age); // kingcos 27
}
function func2({name: nameVar, age: ageVar}) {
console.log(arguments); // Arguments [{name: 'kingcos', age: 27}, callee: (...), Symbol(Symbol.iterator): ƒ]
console.log(nameVar, ageVar); // kingcos 27
}
func1(obj7);
func2(obj7);
ES6 的类仅仅是封装了 ES5.1 构造函数加原型继承的语法糖。
创建对象:
// 工厂模式
function objFatory(name) {
let o = new Object();
o.name = name;
return o;
}
let obj1 = objFatory("kingcos");
let obj2 = objFatory("kingcos.me");
// 构造函数,一般函数名首字母要大写
function Obj(name) {
// 区别:
// 没有显式地创建对象;
// 属性和方法直接赋值给了 this;
// 没有 return.
this.name = name;
}
// new 操作符:
// 在内存中创建一个新对象;
// 构造函数的 prototype 属性赋值到新对象内部的 [[Prototype]] 特性;
// 构造函数内部的 this 指向新对象;
// 执行构造函数内部的代码;
// 如果构造函数返回非空对象,则返回该对象;否则,默认返回刚创建的新对象。
let obj3 = new Obj("kingcos");
let obj4 = new Obj("kingcos.me");
// true
console.log(Obj.prototype === obj3.__proto__);
// constructor 属性指向 Obj
console.log(obj3.constructor == Obj); // true
console.log(obj4.constructor == Obj); // true
// 但一般认为 instanceof 操作符是确定对象类型更可靠的方式
console.log(obj3 instanceof Obj); // true
// 所有自定义对象都继承自 Object
console.log(obj3 instanceof Object); // true
console.log(obj4 instanceof Obj); // true
console.log(obj4 instanceof Object); // true
// 函数表达式也支持作为构造函数
let AnotherObj = function (name) {
this.name = name;
};
let obj5 = new AnotherObj("kingcos");
let obj6 = new AnotherObj; // 不传递参数时可省略 ()
console.log(obj6.name); // undefined
构造函数与普通函数的唯一区别是:调用方式不同,即构造函数使用 new
操作符调用。
function Obj(name) {
this.name = name;
this.printName = function () {
console.log(this.name);
};
}
let obj = new Obj("kingcos");
obj.printName(); // kingcos
// 添加到 window 对象
Obj("kingcos.me");
window.printName(); // kingcos.me
let o = new Object();
// this 将指向 o;关于 call 可见:https://kingcos.me/posts/2023/dive_in_vue_03/
Obj.call(o, "kingcos");
o.printName(); // kingcos
调用一个函数而没有明确设置 this
值时(即没有作为对象的方法调用,或没有使用 call()
/ apply()
调用),this
始终指向 Global
对象(在浏览器中就是 window
对象)。
function Obj(name) {
this.name = name;
// 函数无法共用
this.printName = function () {
console.log(this.name);
};
// 等同于:
// this.printName = new Function("console.log(this.name);");
// 函数在外部,不同对象可以共用
this.outputName = outputName;
}
function outputName() {
console.log(this.name);
}
let obj1 = new Obj("kingcos");
let obj2 = new Obj("kingcos.me");
console.log(obj1.printName === obj2.printName); // false
console.log(obj1.outputName === obj2.outputName); // true
每个函数都会创建一个 prototype
属性,类型为 object
,指向原型对象。定义在原型对象的属性和方法可以被对象实例共享:
function Obj() {}
// 函数表达式同理
// let Obj = function () {};
let obj1 = new Obj();
let obj2 = new Obj();
Obj.prototype.name = "kingcos";
Obj.prototype.printName = function () {
console.log(this.name);
};
console.log(typeof Obj.prototype); // object
console.log(obj1.name); // kingcos
console.log(obj2.name); // kingcos
console.log(obj1.printName === obj2.printName); // true
原型对象默认有一个 constructor
的属性,指回关联的构造函数,两者循环引用:
console.log(Obj.prototype.constructor === Obj); // true
通过构造函数创建的实例,其内部 [[Prototype]]
指针会指向构造函数的原型对象。JavaScript 中没有访问 [[Prototype]]
特性的标准方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露 __proto__
属性,通过这个属性可以访问对象的原型。
注意:
- 实例与构造函数原型之间有直接的关系;但实例与构造函数之间没有关系。即实例与构造函数之间并无继承关系,但却继承自构造函数的原型。
- 构造函数、构造函数的原型、实例是三个完全不同的对象。
- 同一个构造函数创建的不同实例,共享同一个原型对象。
function Obj() {}
console.log(Obj.prototype); // 构造函数的原型对象
let obj1 = new Obj();
console.log(obj1.__proto__); // 实例的原型对象([[Prototype]])
// 在控制台查看 obj1 实例:
// Obj {}
// [[Prototype]] : Object --> 实例内部的 [[Prototype]] === Obj.prototype
// constructor : ƒ Obj()
// [[Prototype]] : Object
console.log(obj1.__proto__ === Obj.prototype); // true
console.log(obj1.__proto__.constructor === Obj); // true
原型链:
function Obj() {}
let obj = new Obj();
// 1. 实例的原型 === 构造函数的原型
console.log(obj.__proto__ === Obj.prototype); // true
// 2. 构造函数原型的原型 === Object 的原型
console.log(Obj.prototype.__proto__ === Object.prototype); // true
// 3. Object 原型的原型 === null
console.log(Object.prototype.__proto__ === null); // true
// instanceof:左侧是否为右侧的实例对象(如何自己实现一个 instanceof?)
console.log(obj instanceof Obj); // true
console.log(obj instanceof Object); // true
console.log(Obj.prototype instanceof Object); // true
// isPrototypeOf:参数的原型([[Prototype]])是否指向调用者
// Obj.prototype === obj.__proto__
console.log(Obj.prototype.isPrototypeOf(obj)); // true
// Object.getPrototypeOf():返回参数的原型([[Prototype]])
console.log(Object.getPrototypeOf(obj) == Obj.prototype); // true
Object.setPrototypeOf()
可以改变实例的原型([[Prototype]]
),但可能会严重影响代码性能。
let a = { name: "a" };
let b = { age: 18 };
// 等同于:
// b.__proto__ = a;
Object.setPrototypeOf(b, a);
console.log(b.name); // a
console.log(Object.getPrototypeOf(b) === a); // true
// 为解决性能问题,可使用 Object.create()
// 创建一个新对象 c,并将其原型指定为 a
let c = Object.create(a);
console.log(c.name); // a
console.log(Object.getPrototypeOf(c) === a); // true
原型的查找层级:
function Obj() {}
let obj = new Obj();
Object.prototype.name = "kingcos";
// 搜索顺序:
// 1. 实例是否有该属性?有则返回,无则继续
// 2. 顺原型链查找:Obj 原型是否有该属性?有则返回,无则继续
// 3. Object 原型是否有?有则返回,无则继续
console.log(obj.name === Object.prototype.name); // true
// 只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性
obj.name = null;
console.log(obj.name); // null
// hasOwnProperty():调用者的某个属性是否在其实例本身
console.log(obj.hasOwnProperty("name")); // true
// Object.getOwnPropertyDescriptor:获取实例属性(而非原型)的属性描述符
// {value: null, writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj, "name"));
delete obj.name;
console.log(obj.name); // kingcos
console.log(obj.hasOwnProperty("name")); // false
console.log(Object.getOwnPropertyDescriptor(obj, "name")); // undefined
原型和 in
操作符:
function Obj() {}
let obj = new Obj();
Object.prototype.name = "kingcos";
console.log("name" in obj); // true
console.log(obj.hasOwnProperty("name")); // false
obj.name = "obj";
console.log("name" in obj); // true
console.log(obj.hasOwnProperty("name")); // true
// 判断是否是原型属性(true 即是)
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
for-in
:
function Obj() {}
Object.defineProperty(Obj.prototype, "age", {
enumerable: false
});
Obj.prototype.name = "kingcos";
Obj.prototype.printName = function () {
console.log(this.name);
};
Obj.prototype.age = 18;
let obj = new Obj();
// 遍历可以被枚举的属性(false 时则不会返回 age)
for (let i in Obj.prototype) {
// name
// printName
console.log(i)
}
// ['name', 'printName']
console.log(Object.keys(Obj.prototype));
// Object.getOwnPropertyNames():列出所有属性(包括不可枚举)
// ['constructor', 'age', 'name', 'printName']
console.log(Object.getOwnPropertyNames(Obj.prototype));
// Object.getOwnPropertySymbols():列出所有符号(Symbol,ES6)
let k = Symbol("k");
let o = { [k]: "k" };
// [Symbol(k)]
console.log(Object.getOwnPropertySymbols(o));
console.log(Object.keys(obj)); // []
obj.name = "obj";
console.log(Object.keys(obj)); // ['name']
属性枚举顺序:
for-in
循环和Object.keys()
的顺序不确定Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
和Object.assign()
的顺序确定:先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中定义的键以它们逗号分隔的顺序插入。
let k1 = Symbol("k1"),
k2 = Symbol("k2");
let o = {
1: 1,
first: "first",
[k1]: "k1",
second: "second",
0: 0
};
o[k2] = "k2";
o[3] = 3;
o.third = "third";
o[2] = 2;
// ['0', '1', '2', '3', 'first', 'second', 'third']
console.log(Object.getOwnPropertyNames(o));
// [Symbol(k1), Symbol(k2)]
console.log(Object.getOwnPropertySymbols(o));
对象迭代:
let k = Symbol("k");
const o = {
name: "o",
age: 0,
job: {},
[k]: "k"
};
// 符号属性将被忽略
console.log(Object.values(o)); // ['o', 0, {…}]
console.log(Object.entries(o)); // [['name', 'o'], ['age', 0], ['job', {…}]]
// Object.values & Object.entries 执行浅复制
console.log(Object.values(o)[2] === o.job); // true
console.log(Object.entries(o)[2][1] === o.job); // true
其他原型语法:
function Obj() {}
console.log(Obj.prototype); // {constructor: ƒ}
// 通过字面量「重写」原型,此时 constructor 将指向 Object 原型的 constructor
Obj.prototype = {
// constructor: Obj, // 也可专门设置回构造函数,但此时该属性的 [[Enumerable]] 为 true(原本默认为 false)
name: "kingcos",
age: 27,
printName() {
console.log(this.name);
}
};
// 或者使用以下方式:
// Object.defineProperty(Obj.prototype, "constructor", {
// enumerable: false,
// value: Obj
// });
// true
console.log(Obj.prototype.constructor === Object.prototype.constructor);
let obj = new Obj();
console.log(obj instanceof Obj); // true
console.log(obj instanceof Object); // true
// 此时将不能再使用 constructor 判断实例的类型
console.log(obj.constructor === Obj); // false
console.log(obj.constructor === Object) // true
第十章 - 函数
定义函数的方式:
// 函数
function a(arg) {
console.log(arg);
}
// 函数表达式
let b = function (arg) {
console.log(arg);
};
// 箭头函数(Arrow Function),ES6
let c = (arg) => {
console.log(arg);
};
// Function 构造函数,不推荐
let d = new Function("arg", "console.log(arg);");
a("a"); // a
b("b"); // b
c("c"); // c
d("d"); // d
箭头函数不能使用 arguments
、super
和 new.target
,也不能用作构造函数。此外,箭头函数也没有 prototype
属性。