专注、坚持

《JavaScript 高级程序设计》笔记

2023.03.16 by kingcos

前言

第一章 -

第二章 -

第三章 - 语言基础

第八章 - 对象、类与面向对象编程

对象的创建方式:

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.definePropertyObject.defineProperties 定义属性及其特性,也可以通过 Object.getOwnPropertyDescriptorObject.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)。另外,需要注意的是:

  1. 对源对象所执行的是浅复制(即引用)
  2. 对于多个源对象中的相同属性,使用最后一个复制的值
  3. gettersetter 不会被合并,仅合并静态值
  4. 合并过程中若抛出错误,将可能只有部分属性得到合并
// 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__ 属性,通过这个属性可以访问对象的原型。

注意:

  1. 实例与构造函数原型之间有直接的关系;但实例与构造函数之间没有关系。即实例与构造函数之间并无继承关系,但却继承自构造函数的原型。
  2. 构造函数、构造函数的原型、实例是三个完全不同的对象。
  3. 同一个构造函数创建的不同实例,共享同一个原型对象。
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']

属性枚举顺序:

  1. for-in 循环和 Object.keys() 的顺序不确定
  2. 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

箭头函数不能使用 argumentssupernew.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。