专注、坚持

《JavaScript 高级程序设计》笔记

2023.03.16 by kingcos

前言

第一章 -

第二章 -

第三章 - 语言基础

第七章 - 迭代器与生成器

迭代几种方式:

// for in 遍历对象
const obj = {
  name: "kingcos",
  age: 18
};

for (let key in obj) {
  console.log(key + ": " + obj[key]); // name: kingcos // age: 18
}

// for loop
const arr = [1, 2, 3];

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]); // 1 // 2 // 3
}

// for of
for (let item of arr) {
  console.log(item); // 1 // 2 // 3
}

// Array.prototype.forEach()
arr.forEach(function (item, index, array) {
  console.log(item); // 1 // 2 // 3
});

JavaScript 中的集合 Set 可以按照元素插入顺序访问:

let set = new Set().add(3).add(1).add(4);

for (let e of set) {
  console.log(e); // 3 // 1 // 4
}

迭代器(Iterator)模式:可迭代的对象需要实现 Iterable 接口;字符串、数组、映射、集合、arguments 对象、NodeList 等 DOM 集合类型均实现了该接口。

let num = 1;
let obj = {};

// 未实现 Iterable
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined

let arr = [1, 2, 3];
console.log(arr[Symbol.iterator]);   // ƒ values() { [native code] }
// 工厂函数显式生成了迭代器
console.log(arr[Symbol.iterator]()); // Array Iterator {}

实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括在:for-of 循环、数组解构、扩展操作符、Array.from()、创建集合、创建映射、Promise.all() 接收由期约组成的可迭代对象、Promise.race() 接收由期约组成的可迭代对象、yield*操作符,在生成器中使用。

let arr = [1, 2, 3];

// 数组解构
let [a, b, c] = arr;
// 扩展操作符
let arr2 = [...arr];
// Array.from()
let arr3 = Array.from(arr);
// Set 构造函数
let set = new Set(arr);
// 继承父类实现的 Iterable
class SubArr extends Array {}
let newArr = new SubArr(1, 2, 3);
for (let e of newArr) {
  console.log(e); // 1 // 2 // 3
}

迭代器协议:

  • 每次成功调用 next(),都会返回一个 IteratorResult 对象;
  • 不同迭代器的实例相互之间没有联系;
  • 迭代器仅仅是使用游标来记录遍历可迭代对象的历程;
  • 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象;
  • Symbol.iterator 属性引用的工厂函数会返回相同的迭代器;
  • 因为迭代器也实现了 Iterable 接口,所以也可以使用 for-of 循环:
let arr = [1, 2];

// 迭代器工程函数
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // Array Iterator {}

console.log(iter.next()); // {value: 1, done: false} 
// iter 记录迭代到第 2 个元素(下标为 1)
console.log(iter.next()); // {value: 2, done: false}

// 新元素插入到下标为 1 的位置
arr.splice(1, 0, 3);
console.log(arr); // 1 3 2

console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: undefined, done: true}

// 数组实现了 Iterable 接口,但迭代器本身也实现了 Iterable 接口
let iter2 = arr[Symbol.iterator]();
for (let e of iter2) {
  console.log(e); // 1 // 3 // 2
}

自定义迭代器:实现 Iterator 接口。

class Counter {
  constructor(limit) {
    this.limit = limit;
  }

  // 具有 [Symbol.iterator]() 属性,可迭代
  [Symbol.iterator]() {
    // 计数器放在迭代器实例内部,将可一一对应,不互相干扰
    let count = 1, limit = this.limit;
    return {
      // next 方法
      next() {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
      // return 方法,可选择性实现,迭代器关闭时被自动调用的
      // 但不是所有迭代器都是可关闭的
      return () {
        console.log('returned');
        return { done: true };
      }
    };
  }
}

let c = new Counter(3);
for (let e of c) {
  if (e > 2) {
    break;
  }
  console.log(e);
}
// 1
// 2
// returned

let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();

iter.return = function () {
  // 虽然数组的迭代器的 return 被调用,但并不会将迭代器强制进入关闭状态
  console.log('returned');
  return { done: true };
};

for (let e of iter) {
  if (e > 2) {
    break;
  }
  console.log(e);
}
// 1
// 2
// returned

for (let e of iter) {
  console.log(e);
}
// 4
// 5

生成器:生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。

  • 箭头函数不能用来定义生成器函数
  • 生成器对象一开始处于暂停执行 suspended 的状态
  • 生成器对象实现了 Iterator 接口,即具有 next() 方法,调用该方法会让生成器开始或恢复执行
// 生成器函数的定义 / 声明
function* genFunc1() {}
let genFunc2 = function* () {
  console.log("genFunc2 invoked");
  return "genFunc2";
};
let foo = {
  * genFunc3() {}
};
class Foo {
  * genFunc4() {}
  static * genFunc5() {}
}

// 生成器对象
const g = genFunc1();
console.log(g);        // genFunc1 {<suspended>}
// next 类似于迭代器
console.log(g.next);   // ƒ next() { [native code] }
// 函数体为空的生成器函数中间不会停留,调用一次 next() 就会让生成器到达 done: true 状态
console.log(g.next()); // {value: undefined, done: true}

const g2 = genFunc2();

// 生成器函数只会在初次调用 next() 方法后开始执行
// genFunc2 invoked
// value 属性是生成器函数的返回值,默认值为 undefined
// {value: 'genFunc2', done: true}
console.log(g2.next());

// 生成器对象实现了 Iterable 接口,它们默认的迭代器是自引用的
console.log(g === g[Symbol.iterator]());  // true
console.log(genFunc1()[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }

yield

  • yield 关键字可以让生成器停止和开始执行
  • 生成器函数在遇到 yield 关键字之后,执行会停止,函数作用域的状态会被保留
  • 停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行
function * genFunc() {
  console.log("before yield");
  yield;
  console.log("after yield");
}

let genObj = genFunc();

// before yield
// {value: undefined, done: false}
// 停止执行
console.log(genObj.next());

// after yield
// {value: undefined, done: true}
// 恢复执行
console.log(genObj.next()); 

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

对象的创建方式:

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 Obj() {}
let obj1 = new Obj();

// 重写原型
Obj.prototype = {
  constructor: Obj,
  foo() {
    console.log("foo")
  }
};

let obj2 = new Obj();

// obj1 中指向原型的指针仍然引用的时最初的原型
// obj1.foo(); // Uncaught TypeError: obj.foo is not a function
obj2.foo(); // foo

// 原型也广泛使用于原生类型中
console.log(typeof Array.prototype.sort);       // function
console.log(typeof String.prototype.substring); // function

// 可用通过原生引用类型的原型定义方法
String.prototype.startsWith = function (text) {
  return this.indexOf(text) === 0;
};

// 但可能存在命名冲突,因此不推荐(推荐:创建一个自定义的类,并继承原生类型)
console.log("kingcos".startsWith("k")); // true

原型的问题在于共享引用类型的属性也将在所有实例中共享,但一般不同的实例应当有属于自己的属性副本。

ECMAScript 仅支持实现继承(即不支持接口继承),且主要是通过原型链实现。

function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function () {
  console.log(this.name + " is eating.");
};

function Cat() {
  this.name = "Cat";
}

function Dog() {
  
}

// Cat & Dog 继承自 Animal
Cat.prototype = new Animal();
Dog.prototype = new Animal();

let cat = new Cat();
// 原型链:cat 实例 -> animal 实例 -> Animal.prototype -> Object.prototype -> null

// 原型链的问题:子类型实例化时不能给父类型的构造函数传参
let dog = new Dog("Dog");
dog.eat(); // undefined is eating.

// eat 继承自 Animal
cat.eat(); // Cat is eating.
// toString 继承自 Object
console.log(cat.toString()); // [object Object]

// 同理,应使用 instanceof 判断类型,即 cat 是 Cat、Animal、Object 的实例
console.log(cat.constructor === Cat);    // false
console.log(cat.constructor === Animal); // true
console.log(cat instanceof Cat);         // true
console.log(cat instanceof Animal);      // true
// 也可使用 isPrototypeOf 判断是否是实例的原型
console.log(Cat.prototype.isPrototypeOf(cat));    // true
console.log(Animal.prototype.isPrototypeOf(cat)); // true
console.log(Object.prototype.isPrototypeOf(cat)); // true

原型链的问题:子类型实例化时不能给父类型的构造函数传参、原型中包含引用值的问题;因此原型链基本不会被单独使用。

盗用构造函数(constructor stealing):

function Animal(name) {
  this.actions = ["eat", "run"];
  this.name = name;
}

function Cat() {
  // Cat 类型的实例也拥有了 Animal 的属性
  // 也可以在子类构造函数中向父类构造函数传参
  Animal.call(this, "Cat");
}

function Dog(name) {
  Animal.call(this, name);
}

let cat = new Cat();
cat.actions.push("miao");

let dog = new Dog("Dog");
dog.actions.push("wang");

console.log(cat.actions); // ['eat', 'run', 'miao']
console.log(dog.actions); // ['eat', 'run', 'wang']
console.log(cat.name);    // Cat
console.log(dog.name);    // Dog

盗用构造函数的问题:必须在构造函数中定义方法,因此函数不能重用;子类也不能访问父类原型上定义的方法;因此盗用构造函数基本也不会被单独使用。

组合继承:结合了原型链 + 盗用构造函数。

function Animal(name) {
  this.actions = ["eat", "run"];
  this.name = name;
}

Animal.prototype.eat = function () {
  console.log(this.name + " is eating.");
};

// 继承属性
function Cat() {
  Animal.call(this, "Cat");   // 第二次调用父类构造函数(遮盖了第一次调用后的属性)
}
// 继承方法
Cat.prototype = new Animal(); // 第一次调用父类构造函数(多余)

function Dog(name) {
  Animal.call(this, name);
}

Dog.prototype = new Animal();

let cat = new Cat();
cat.actions.push("miao");
cat.eat(); // Cat is eating.

let dog = new Dog("Dog");
dog.actions.push("wang");
dog.eat(); // Dog is eating.

console.log(cat.actions); // ['eat', 'run', 'miao']
console.log(dog.actions); // ['eat', 'run', 'wang']
console.log(cat.name);    // Cat
console.log(dog.name);    // Dog

原型式继承:适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

let animal = {
  name: "",
  arr: [1]
};

// 原型链:animal2 实例 -> animal
let animal2 = object(animal);
console.log(animal2.__proto__ === animal); // true

animal2.name = "2";
animal2.arr.push(2);

let animal3 = object(animal);
animal3.arr.push(3);

// 适用于已有一个对象,并希望在其基础上创建一个新对象
// 需要注意:原对象的引用值属性将被共享
console.log(animal.arr);  // [1, 2, 3]
console.log(animal2.arr); // [1, 2, 3]
console.log(animal3.arr); // [1, 2, 3]

// ES5: Object.create,类似 object(),第二个参数与 Object.defineProperties() 的第二个参数一样
let animal4 = Object.create(animal, {
  name: {
    value: "4"
  }
});
console.log(animal4.name); // 4
console.log(animal4.__proto__ === animal); // true

animal4.arr.push(4);
console.log(animal.arr);  // [1, 2, 3, 4]
console.log(animal4.arr); // [1, 2, 3, 4]

寄生式继承(Parasitic Inheritance):适合主要关注对象,而不在乎类型和构造函数的场景

// 这里非必需,任何返回新对象的函数均可用
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function create(o) {
  let clone = object(o);
  // 增强对象
  clone.sayHi = function () {
    console.log("hi")
  };
  return clone;
}

let animal = {
  name: "",
  arr: [1]
};

// animal2 新对象具有 animal 的所有属性和方法,也有增强的 sayHi
let animal2 = create(animal);
animal2.sayHi(); // hi

寄生式组合继承:是引用类型继承的最佳模式。

// 组合继承效率问题:父类构造函数始终会被调用两次
// 1. prototype 赋值时,new 构造一次;
// 2. 子类实例构造时,在子类构造函数中会再次调用。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function inheritPrototype(child, parent) {
  // cat 实例 -> Cat 原型(child 原型,prototype)-> Animal 原型(parent.prototype)
  let prototype = object(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;

  console.log(Cat.prototype === prototype); // true
}

function Animal(name) {
  this.name = name;
  this.arr = []
}

Animal.prototype.printName = function () {
  console.log(this.name);
};

// 继承属性
function Cat(name, age) {
  Animal.call(this, name); // 仅调用一次父类构造函数
  this.age = age;
}

inheritPrototype(Cat, Animal);

Cat.prototype.printAge = function () {
  console.log(this.age);
};

let cat = new Cat("Cat", 1);

console.log(cat.__proto__ === Cat.prototype); // true
console.log(Cat.prototype.__proto__ === Animal.prototype); // true

类(class):ES6 引入,本质是基于原型和构造函数的语法糖。

// class 不支持命名提升
// console.log(A); // Uncaught ReferenceError: Cannot access 'A' before initialization
class A {}
console.log(A); // class A {}

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果 不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。调用类构造函数时如果 忘了使用 new 则会抛出错误。

第十章 - 函数

定义函数的方式:

// 函数
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 属性。