前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript 高级程序设计(第 4 版)- 对象、类与面向对象编程

JavaScript 高级程序设计(第 4 版)- 对象、类与面向对象编程

作者头像
Cellinlab
发布2023-05-17 15:00:28
5110
发布2023-05-17 15:00:28
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

ECMA-262 将对象定义为一组属性的无序集合。即对象是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,该名称映射到一个值。

# 理解对象

创建自定义对象的通常方式是创建Object的一个新实例,然后再给它添加属性和方法。也可以通过对象字面量方法创建。

# 属性的类型

ECMA-262使用一些内部特性来描述属性的特征,开发者不能在JS中直接访问这些特性,为了将某个特性标识为内部特性,会用中括号将特性名称括起来。

# 数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取和写入。数据属性有 4个特性描述它们的行为。

  • [[Configurable]]: 属性是否可以通过delete删除并重新定义,是否可以修改它的特性,是否可以把它改为访问器属性,默认true
  • [[Enumerable]]: 属性是否可通过for-in循环返回,默认true
  • [[Writable]]: 是否可以被修改,默认true
  • [[Value]]: 包含属性实际值,即读取和写入属性值的位置,默认为undefined

要修改属性的默认特性,就必须使用Object.defineProperty()方法,接收3个参数:要给其添加属性的对象、属性名称和描述符对象。描述符对象属性可包含:configurable、enumerable、writable和value

# 访问器属性

访问器属性不包含数据值。相反,它们包含一个getter函数和一个setter函数,二者都非必须。访问器属性有 4 个特性描述它们的行为。

  • [[Configurable]]: 属性是否可以通过delete删除并重新定义,是否可以修改它的特性,是否可以把它改为数据属性,默认是true
  • [[Enumerable]]: 属性是否可通过for-in循环返回,默认是true
  • [[Get]]: 获取函数,读取属性时调用,默认undefined
  • [[Set]]:设置函数,写入属性时调用,默认undefined

访问器属性不能直接定义,必须使用Object.defineProperty()

代码语言:javascript
复制
let book = {
  year_: 2017,
  edition: 1
};
Object.defineProperty(book, "year", {
  get() {
    return this._year;
  },
  set(newValue) {
    if (newValue > 2017) {
      this._year = newValue;
      this.edition += newValue - 2017;
    }
  }
});
book.year = 2018;
console.log(book.edition); // 2

# 定义多个属性

  • Object.defineProperties(),接收两个参数:目标对象和描述符对象
    • 数据属性的configurable、enumerable和writable特性值都是false
代码语言:javascript
复制
let book = {};
Object.defineProperties(book, {
  year_: {
    value: 2017,
  },
  edition: {
    value: 1,
  },
  year: {
    get() {
      return this.year_;
    },
    set(newValue) {
      if (newValue > 2017) {
        this.year_ = newValue;
        this.edition += newValue - 2017;
      }
    }
  }
});

# 读取属性的特性

  • Object.getOwnPropertyDescriptor()可取得指定属性的属性描述符,接收两个参数:目标对象和目标属性名。返回一个描述符对象。
    • 返回对象,对于访问器属性包含configurable、enumerable、get 和 set属性
    • 返回对象,对于数据属性包含configurable、enumerable、writable 和 value属性
代码语言:javascript
复制
let descriptor = Object.getOwnPropertyDescriptor(book, 'year_');
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, 'year');
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"

  • ES2017新增Object.getOwnPropertyDescriptors()静态方法,实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor(),并在一个新对象中返回他们
代码语言:javascript
复制
console.log(Object.getOwnPropertyDescriptors());
// {
//   edition: {
//     configurable: false,
//     enumerable: false,
//     value: 1,
//     writable: false
//   },
//   year: {
//     configurable: false,
//     enumerable: false,
//     get: f(),
//     set: f(newValue),
//   },
//   year_: {
//     configurable: false,
//     enumerable: false,
//     value: 2017,
//     writable: false
//   }
// }

# 合并对象

  • Object.assign(),接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。
    • 以字符串和符号为键的属性会被复制
    • 对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值
代码语言:javascript
复制
let dest, src, result;
/**
 * 简单复制
 */
dest = {};
src = { id: 'src' };

result = Object.assign(dest, src);

// Object.assign修改目标对象,返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: 'src' }
console.log(dest); // { id: 'src' }

/**
 * 多个源对象
 */
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: 'foo', b: 'bar' }

/**
 * 获取函数与设置函数
 */
dest = {
  set a(val) {
    console.log(`Invoked dest setter with parm ${val}`);
  }
};
src = {
  get a() {
    console.log('Invoked src getter');
    return 'foo';
  }
};

Object.assign(dest, src);
// 调用src的获取方法
// 调用dest的设置方法,并传入参数'foo'
// 因为此处设置函数不执行赋值操作,所以实际上并没有把值转移过来
console.log(dest); //

  • Object.assign()实际上对每个源对象执行的是浅复制
  • 如果多个源对象都有相同的属性,则使用最后一个复制的值
  • 从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象(即不能在两个对象间转移获取函数和设置函数)
  • 如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()没有“回滚”之前赋值的概念,即其为一个尽力而为、可能只完成部分赋值的方法
代码语言:javascript
复制
let dest, src, result;
/**
 * 覆盖属性
 */
dest = { id: 'dest' };
result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
// Object.assign会覆盖重复的属性
console.log(result); // { id: 'src1', a: 'foo', b: 'bar' }
// 可以通过在目标对象上的设置函数观察到覆盖过程
dest = {
  set id(x) {
    console.log(x);
  }
};
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
// first second third

/**
 * 对象引用
 */
dest = {};
src = { a: {} };

Object.assign(dest, src);

// 浅复制意味着只会复制对象的引用
console.log(dest); // { a: {} }
console.log(dest.a === src.a); // true

/**
 * 错误处理
 */
dest = {};
src = {
  a: 'foo',
  get b() {
    // Object.assign() 在调用这个获取函数时会抛出错误
    throw new Error();
  },
  c: 'bar'
};
try {
  Object.assign(dest, src);
} catch (error) {}
// Object.assign() 没办法回滚已经完成的修改
// 在出错之前,目标对象上已经完成的修改会继续存在
console.log(dest); // { a: 'foo' }

# 对象标识及相等判定

在ES6之前,有些特殊情况即使===操作符也无能为力。为此,ES6新增了Object.is(),该方法与===很像,但同时考虑一些特殊情况。该方法接收两个比较参数,要比较多个可以递归地利用相等性传递即可。

代码语言:javascript
复制
// === 符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false

// 这些情况在不同JS引擎中表现不同,但被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true

// 要确定NaN的相等性,必须使用极为讨厌的isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true

console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false

// 正确的额0、-0、+0相等/不相等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false

// 正确的NaN相等判定
console.log(Object.is(NaN, NaN)); // true

// 要检查超过两个值,递归地利用相等性传递即可
function recursivelyCheckEqual(x, ...rest) {
  return Object.is(x, rest[0]) &&
    (rest.length< 2 || recursivelyCheckEqual(...rest));
}

# 增强对象语法

ES6为定义和操作对象新增了很多极其有用的语法糖特性。这些特性没有改变现有引擎的行为,但极大提升了处理对象的方便程度。

  • 属性值简写
代码语言:javascript
复制
let name = 'Matt';
let person = { name };
console.log(person); // { name: 'Matt' }

// 代码压缩程序会在不同作用域键保留属性名,以防止找不到引用
function makePerson(name) {
  return {
    name
  };
}
let person2 = makePerson('Matt');
console.log(person2.name); // Matt

  • 可计算属性

在引入可计算属性之前,如果使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性

代码语言:javascript
复制
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';

let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';

console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }

有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为JS表达式而不是字符串来求值

代码语言:javascript
复制
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';

let person = {
  [nameKey]: 'Matt',
  [ageKey]: 27,
  [jobKey]: 'Software engineer'
};

console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }

// 使用复杂表达式,在实例化时再求值
let uniqueToken = 0;
function getUniqueKey(key) {
  return `${key}_${uniqueToken++}`;
}

let person2 = {
  [getUniqueKey(nameKey)]: 'Matt',
  [getUniqueKey(ageKey)]: 27,
  [getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person2); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }

  • 简写方法名
代码语言:javascript
复制
// 方法名 冒号 匿名函数表达式
let person = {
  sayName: function(name) {
    console.log(`My name is ${name}`);
  }
};

// 简写
let person2 = {
  sayName(name) {
    console.log(`My name is ${name}`);
  }
};

// 简写方法名对获取函数和设置函数也适用
let person3 = {
  name_: '',
  get name() {
    return this.name_;
  },
  set name(name) {
    this.name_ = name;
  },
  sayName() {
    console.log(`My name is ${this.name_}`);
  }
}

// 简写方法名与可计算属性键相互兼容
const methodKey = 'sayName';
let person4 = {
  [methodKey](name) {
    console.log(`My name is ${name}`);
  }
};

# 对象解构

ES6新增对象结构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。即对象结构就是使用与对象匹配的结构来实现对象属性赋值

代码语言:javascript
复制
let person = {
  name: 'John',
  age: 18
};

let { name: personName, age: personAge } = person;
console.log(personName); // John
console.log(personAge); // 18

// 如果让变量直接使用属性的名称,可以使用简写语法
let { name, age } = person;

// 解构赋值不一定与对象的属性匹配,赋值的时候可以忽略某些属性,如果引用的属性不存在,则该变量的值就是undefined
let { job } = person;
console.log(job); // undefined

// 也可以在解构赋值的同时定义默认值
let { gender = "male" } = person;
console.log(gender); // male

// 解构在内部使用函数ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象
// 即在对象解构上下文中,原始值会被当成对象,也就说null和undefined不能被解构,否则会抛出错误
let { length } = 'foobar';
console.log(length); // 6

let { constructor: c } = 4;
console.log(c === Number); // true

let { _ } = null; // TypeError
let { _ } = undefined; // TypeError

// 如果给事先声明的变量赋值,则赋值表达式必须包含在一对括号中
let personAddress, personEmail;
let person2 = {
  address: 'Beijing',
  email: 'a@example.cn'
};
({ address: personAddress, email: personEmail } = person2);

  • 嵌套解构
代码语言:javascript
复制
let person = {
  name: 'Matt',
  age: 27,
  job: {
    title: 'Software engineer'
  }
};
let personCopy = {};

({
  name: personCopy.name,
  age: personCopy.age,
  job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给personCopy,修改该对象对copy对象也有影响
person.job.title = 'Hacker';

console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }

// 解构赋值可以使用嵌套解构,以匹配嵌套的属性
let { job: { title }} = person;
console.log(title); // Hacker

// 在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象都一样

  • 部分解构
代码语言:javascript
复制
// 涉及多个属性的解构赋值是一个输出无关的顺序化操作
// 如果一个解构表达式涉及多个赋值,开始的赋值成功而后面出错,则只会完成一部分
let person = {
  name: 'Matt',
  age: 27,
};
let personName, personBar, personAge;
try {
  // person.foo 是 undefined,因此会抛出错误
  ({ name: personName, foo: { bar: personBar }, age: personAge } = person);
} catch (e) {}

console.log(personName, personBar, personAge);
// Matt, undefined, undefined

  • 参数上下文匹配

在函数参数列表中也可以进行解构赋值,对参数的解构不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量

代码语言:javascript
复制
let person = {
  name: 'Matt',
  age: 27
};
function printPerson(foo, {name, age}, bar) {
  console.log(arguments);
  console.log(name, age);
}
function printPerson2(foo, { name: personName, age: personAge}, bar) {
  console.log(arguments);
  console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27

printPerson('2nd', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27

# 创建对象

ES6开始正式支持类和继承。ES6的类旨在完全涵盖之前规范设计的基于原型的继承模式。ES6的类仅仅是封装了ES5.1构造函数加原型继承的语法糖而已。

# 工厂模式

代码语言:javascript
复制
// 工厂模式用于抽象创建特定对象的过程
function createPerson(name, age, job) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name);
  };
  return o;
}
let p1 = createPerson('Nicholas', 29, 'Software engineer');
let p2 = createPerson('Greg', 27, 'Doctor');

工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

# 构造函数模式

ECMAScript中的构造函数是用于创建特定类型对象的。像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。也可以自定义构造函数,以函数形式为自己的对象类型定义属性和方法。

代码语言:javascript
复制
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
  };
}
let person1 = new Person('Nicholas', 29, 'Software engineer');
let person2 = new Person('Greg', 27, 'Doctor');
// Person()构造函数代替了createPerson()工厂函数。实际上,Person()内部的代码跟createPerson()基本是一样的
// person1和person2分别保存着Person的不同实例,这两个对象都有一个constructor属性指向Person
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
// constructor本来是用于表示对象类型的。不过,一般认为instanceof操作符是确定对象类型更可靠的方式
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

  • 构造函数和工厂方法区别
    • 没有显式地创建对象
    • 属性和方法直接赋值给了this
    • 没有return
    • 函数名大写(约定俗成构造函数名称首字母大写)
  • 使用new创建实例
    1. 在内存中创建一个新对象
    2. 新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性
    3. 构造函数内部的this被赋值给新对象
    4. 执行构造函数内部的代码(给新对象添加属性)
    5. 如果构造函数返回非空对象,就返回该对象;否则,返回刚创建的对象
  • 相比于工厂模式,自定义构造函数可以确保实例被标识为特定类型
  • 构造函数不一定要写成函数声明的形式,赋值给变量的函数表达式也可以表示构造函数
代码语言:javascript
复制
let Person = function(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
  };
}
let person1 = new Person('Nicholas', 29, 'Software engineer');
let person2 = new Person('Greg', 27, 'Doctor');

person1.sayName(); // Nicholas
person2.sayName(); // Greg

  • 实例化时,如果不想传参数,构造函数后面的括号可加可不加。只要有new操作符,就可以调用相应的构造函数
代码语言:javascript
复制
function Person() {
  this.name = "Jake";
  this.sayName = function() {
    console.log(this.name);
  };
}
let person1 = new Person();
let person2 = new Person;

person1.sayName(); // Jake
person2.sayName(); // Jake

  • 构造函数也是函数
    • 和普通函数的区别就是调用方法不一样
    • 任何函数使用new操作符调用就是构造函数
    • 调用一个函数而没有明确设置this值的情况下,this始终指向Global对象
代码语言:javascript
复制
// 作为构造函数
ley person = new Person('Nicholas', 29, 'Software engineer');
person.sayName(); // Nicholas

// 作为函数调用
Person('Greg', 27, 'Doctor'); // 添加到window对象
window.sayName(); // Greg

// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, 'Kristen', 25, 'Nurse');
o.sayName(); // Kristen

构造函数的问题

  • 定义的方法会在每个实例上都创建一遍,不同实例上的函数虽然同名却不相等
代码语言:javascript
复制
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}

  • 都是做一样的事,所以没必要定义两个不同的Function实例。可以通过把函数定义转移到构造函数外部来解决
代码语言:javascript
复制
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName;
}
function sayName() {
  console.log(this.name);
}
// 此处sayName() 被定义在了构造函数外部,在函数内部,sayName属性等于全局sayName()函数。
// 因为sayName属性中包含的只是一个指向外部函数的指针,所以p1和p2共享了定义在全局作用域上的sayName()函数
// 但是这种方案会导致全局作用域被搞乱,因为该函数只能在一个(类?)对象上调用,这个问题可以通过原型模式解决

let p1 = new Person('Nicholas', 29, 'Software engineer');
let p2 = new Person('Greg', 27, 'Doctor');

p1.sayName(); // Nicholas
p2.sayName(); // Greg

# 原型模式

每个函数都会创建一个prototype属性,该属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,该对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

代码语言:javascript
复制
function Person() {}
// 也可以使用函数表达式 如 let Person = function() {};

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function() {
  console.log(this.name);
};

let p1 = new Person();
p1.sayName(); // "Nicholas"

let p2 = new Person();
p2.sayName(); // "Nicholas"

console.log(p1.sayName == p2.sayName); // true

理解原型

  • 只要创建一个函数,就会按照特定的规则为该函数创建一个prototype属性(指向原型对象)
  • 默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数
  • 在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object
  • 每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象
    • [[Prototype]]特性目前没有标准的访问方式,在Firefox、Safari和Chrome中可以通过__proto__属性,访问对象的原型。在其他实现中,这个特性完全被隐藏了
代码语言:javascript
复制
/**
 * 构造函数可以是函数表达式
 * 也可以是函数声明
 * function Person() {}
 * let Person = function() {}
 */
 function Person() {}
 /**
  * 声明之后,构造函数就有一个与之关联的原型对象
  */
  console.log(typeof Person.prototype);
  console.log(Person.prototype);
  // object
  // {
  //  constructor: f Person(),
  //  __proto__: Object
  //}
  /**
   * 构造函数有一个prototype属性,引用其原型对象
   * 而这个原型对象也有一个constructor属性,引用这个构造函数
   * 即两者循环引用
   */
  console.log(Person.prototype.constructor === Person); // true

  /**
   * 正常的原型链都会终止于Object的原型对象
   * Object原型的原型是null
   */
  console.log(Person.prototype.__proto__ === Object.prototype); // true
  console.log(Person.prototype.__proto__.constructor === Object); // true
  console.log(Person.prototype.__proto__.__proto__ === null); // true

  console.log(Person.prototype.__proto__);
  // {
  //  constructor: f Object(),
  //  toString: ...
  //  hasOwnProperty: ...
  //  isPrototypeOf: ...
  //  ...
  //}

  let p1 = new Person();
  let p2 = new Person();
  /**
   * 构造函数、原型对象和实例是3个完全不同的对象
   */
  console.log(p1 !== Person); // true
  console.log(p1 !== Person.prototype); // true
  console.log(Person.prototype !== Person); // true

  /**
   * 实例通过__proto__链接到原型对象,实际上指向隐藏特性[[Prototype]]
   * 构造函数通过prototype属性链接到原型对象
   * 实例与构造函数没有直接联系,与原型对象有直接联系
   */
  console.log(p1.__proto__ === Person.prototype); // true
  console.log(p1.__proto__.constructor === Person); // true

  /**
   * 同一个构造函数创建的两个实例,共享一个原型对象
   */
  console.log(p1.__proto__ === p2.__proto__); // true

  /**
   * instanceof 检查实例的原型链中是否也包含指定构造函数的原型
   */
  console.log(p1 instanceof Person); // true
  console.log(p1 instanceof Object); // true
  console.log(p1.prototype instanceof Object); // true

  • Object.getPrototypeOf(),返回参数内部的特性[[Prototype]]的值,即获取对象的原型
代码语言:javascript
复制
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
console.log(Object.getPrototypeOf(p1).name); // "Nicholas"

  • Object.setPrototypeOf(),可以向实例的私有特性[[Prototype]]写入一个新值。通过该方法可以重写一个对象的原型继承关系
代码语言:javascript
复制
let biped = {
  numLegs: 2
};
let person = {
  name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true

  • Object.setPrototypeOf()可能会造成性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型
代码语言:javascript
复制
let biped = {
  numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true

原型层级

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应值。

代码语言:javascript
复制
function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function(){
  console.log(this.name);
};

let p1 = new Person();
let p2 = new Person();

p1.name = 'Greg';
console.log(p1.name); // 'Greg' 来自实例
console.log(p2.name); // 'Nicholas' 来自原型

  • 只要给对象实例添加一个属性。这个属性就会屏蔽原型对象上的同名属性,即虽然不会修改它但会屏蔽对它的访问
  • 使用delete操作符可以完全删除实例上的属性,从而标识符解析过程能继续搜索原型对象(原型对象属性不能通过delete删除)
  • hasOwnProperty()方法用于确定某个属性是在实例上还是原型对象上,继承自Object,会在属性存在于调用它的对象实例上时返回true
代码语言:javascript
复制
function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function(){
  console.log(this.name);
};

let p1 = new Person();
let p2 = new Person();
console.log(p1.hasOwnProperty('name')); // false

p1.name = 'Greg';
console.log(p1.hasOwnProperty('name')); // true

delete p1.name;
console.log(p1.hasOwnProperty('name')); // false

  • Object.getOwnPropertyDescriptor()只对实例属性有效,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()

原型和in操作符

in可以单独使用或在for-in循环中使用。单独使用时,in会在可以通过对象访问指定属性时返回true,无论该属性在实例上还是原型上。

代码语言:javascript
复制
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.sayName = function() {
  console.log(this.name);
};

let p1 = new Person();
let p2 = new Person();

console.log(p1.hasOwnProperty('name')); // false
console.log("name" in p1); // true

p1.name = 'Greg';
console.log(p1.name); // 'Greg' 来自实例
console.log(p1.hasOwnProperty('name')); // true
console.log("name" in p1); // true

console.log(p2.name); // 'Nicholas' 来自原型
console.log(p2.hasOwnProperty('name')); // false
console.log('name' in p2); // true

delete p1.name;
console.log(p1.name); // 'Nicholas' 来自原型
console.log(p1.hasOwnProperty('name')); // false
console.log('name' in p1); // true

  • 结合hasOwnProperty()和in操作符,确定某个属性是否存在于原型上
代码语言:javascript
复制
function hasPrototypeProperty(object, name) {
  // 只要通过对象可以访问, in 操作符就返回 true
  //  hasOwnProperty()只有属性存在于实例上时才返回 true
  // 对象拥有该属性但是实例没有,则属性在原型上
  return !object.hasOwnProperty('name') && (name in object);
}

  • 在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性
  • 遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回
  • 要获得对象上所有可枚举的实例属性,可使用Object.keys(),接收一个对象作参数,返回包含该对象所有可枚举属性名称的字符串数组
代码语言:javascript
复制
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software engineer';
Person.prototype.sayName = function() {
  console.log(this.name);
};

let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"
let p1 = new Person();
p1.name = 'Rob';
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name, age]"

  • 如果想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames()
代码语言:javascript
复制
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"

  • Object.keys()和Object.getOwnPropertyNames()在适当的时候可以用来代替for-in循环
  • ES6新增Object.getOwnPropertySymbols()
代码语言:javascript
复制
let k1 = Symbol('k1');
let k2 = Symbol('k2');
let o = {
  [k1]: 'k1',
  [k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]

属性枚举顺序

  • for-in循环和Object.keys()的枚举顺序是不确定的,取决于JS引擎,可能因浏览器而异
  • Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assign()的枚举顺序是确定的
    • 先以升序枚举数值键,然后以插入顺序枚举字符串和符号键
    • 在对象字面量中定义的键以它们逗号分隔的顺序插入
代码语言:javascript
复制
let k1 = Symbol('k1');
let k2 = Symbol('k2');

let o = {
  1: 1,
  first: 'first',
  [k1]: 'k1',
  second: 'second',
  0: 0
};

o[k2] = 'sym2';
o[3] = 3;
o.third = 'third';
o[2] = 2;

console.log(Object.getOwnPropertyNames(o));
// ["0", "1", "2", "3", "first", "second", "third"]
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]

# 对象迭代

ECMAScript2017新增两个静态方法,用于将对象内容转换为序列化的可迭代的格式。Object.values()和Object.entries()接收一个对象,返回其内容的的数组。Object.values()返回对象值的数组,Object.entries()返回键/值对的数组。

代码语言:javascript
复制
const o = {
  foo: 'bar',
  baz: 1,
  qux: {}
};
console.log(Object.values(o)); // ['bar', 1, {}]
console.log(Object.entries(o)); // [['foo', 'bar'], ['baz', 1], ['qux', {}]]

// 非字符串会转换为字符串输出
// 两个方法执行对象的浅复制
console.log(Object.values(o)[0] === o.qux); // true
console.log(Object.entries(o)[0][1] === o.qux); // true

// 符号属性会被忽略
const sym = new Symbol();
const o = {
  [sym]: 'foo'
};
console.log(Object.values(o)); // []
console.log(Object.entries(o)); // []

# 其他原型语法

通过字面量来重写原型时,Person.prototype被设置为等于一个通过对象字面量创建的新对象,此时Person.prototype的constructor属性就不指向Person了

  • 创建函数时,也会创建它的prototype对象,同时会自动给这个原型的constructor属性赋值
  • 字面量方法完全重写了默认的prototype对象,因此constructor属性也指向了完全不同的新对象(Object构造函数),不再指向原来的构造函数
  • 此时虽然instanceof操作符还能可靠地返回值,但不能依靠constructor属性来识别类型了
代码语言:javascript
复制
let friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true

  • 如果constructor的值很重要,则可以在重写原型对象时专门设置
代码语言:javascript
复制
function Person() {}
Person.prototype = {
  constructor: Person,
  name: 'Nicholas',
  age: 29,
  job: 'Software engineer',
  sayName() {
    console.log(this.name);
  }
};
// 这种方法恢复constructor属性会创建一个[[Enumerable]]为true的属性
// 原生constructor属性默认是不可枚举的
// 可以改为使用Object.defineProperty()来定义constructor属性
// Object.defineProperty(Person.prototype, 'constructor', {
//   enumerable: false,
//   value: Person
// });

# 原型的动态性

因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。

  • 重写构造函数上的原型之后再创建的实例才会引用新的原型,之前创建的实例仍然会引用最初的原型
# 原生对象原型
  • 所有原生引用类型的构造函数(包括Object、Array、String等)都在原型上定义了实例方法
  • 通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法
代码语言:javascript
复制
console.log(typeof Array.prototype.sort); // function
console.log(typeof String.prototype.substring); // function

String.prototype..startsWith = function(str) {
  return this.indexOf(str) === 0;
};
let msg = "Hello world";
console.log(msg.startsWith('Hello')); // true

# 原型的问题
  • 弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
  • 原型的最主要的问题源自它的共享特性,该特性对函数和包含原始值的属性还好。真正的问题来自包含引用值的属性
代码语言:javascript
复制
function Person() {}
Person.prototype = {
  constructor: Person,
  name: 'Nicholas',
  friends: ['Shelby', 'Court'],
  sayName() {
    console.log(this.name);
  }
}
let p1 = new Person();
let p2 = new Person();

p1.friends.push('Van');

console.log(p1.friends); // Shelby,Court,Van
console.log(p2.friends); // Shelby,Court,Van
console.log(p1.friends === p2.friends); // true

# 继承

很多面向对象的语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。实现继承是ECMAScript唯一支持的继承方式,而这主要是通过原型链实现的。

# 原型链

ECMA-262把原型链定义为ECMAScript的主要继承方式。基本思想就是通过原型继承多个引用类型的属性和方法。每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。

代码语言:javascript
复制
function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
};
function subType() {
  this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
  return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true

# 默认原型

默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。任何函数的默认原型都是一个Object实例,即这个实例内部指针指向Object.prototype。

# 原型与继承关系
  • instanceof操作符,如果一个实例的原型链中出现过相应的构造函数,则instanceof返回true
  • isPrototypeOf()方法,原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,该方法就返回true
代码语言:javascript
复制
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true

console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true

# 关于方法

子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,必须在原型赋值之后在添加到原型上。

代码语言:javascript
复制
function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
};
function SubType() {
  this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
// 新方法
SubType.prototype.getSubValue = function() {
  return this.subproperty;
};
// 覆盖已有的方法
SubType.prototype.getSuperValue = function() {
  return false;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // false

以对象字面量方式创建原型方法会破坏之前的原型链,因为相当于重写了原型链

代码语言:javascript
复制
function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
};
function SuperType() {
  this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
// 通过字面量添加新方法,会使上一行无效
SubType.prototype = {
  getSuperValue() {
    return this.subproperty;
  },
  someOtherMethod() {
    return false;
  }
};
let instance = new SubType();
console.log(instance.getSuperValue()); // Error

# 原型链的问题

原型中包含的应用之会在所有实例间共享,所以属性通常会在构造函数中定义而不是在原型上定义。在使用原型实现继承时,原型实际上会变成另一个类型的实例,即原先的实例属性变为了原型属性。 子类型在实例化时不能给父类型的构造函数传参。事实上,无法在不影响所有对象实例的情况下把参数传进父类的构造函数,再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。

代码语言:javascript
复制
function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // red, blue, green, black

let instance2 = new SubType();
console.log(instance2.colors); // red, blue, green, black

# 盗用构造函数

思路:在子类构造函数中调用父类构造函数。函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。

代码语言:javascript
复制
function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {
  // 继承SuperType
  SuperType.call(this);
}

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // red, blue, green, black

let instance2 = new SubType();
console.log(instance2.colors); // red, blue, green
# 传递参数

相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

代码语言:javascript
复制
function SuperType(name) {
  this.name = name;
}
function SubType() {
  // 继承SuperType并传参
  SuperType.call(this, 'Nicholas');

  // 实例属性
  this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // Nicholas
console.log(instance.age); // 29
# 盗用构造函数

盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,构造函数基本上也不能单独使用。

# 组合继承

伪经典继承,综合了原型链和盗用构造函数。使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。既可以把方法定义在原型上以实现重用,又可以让每个实例有自己的属性。

代码语言:javascript
复制
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

let instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // "red, blue, green, black"
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29

let instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // "red, blue, green"
instance2.sayName(); // "Greg"
instance2.sayAge(); // 27

# 原型式继承

代码语言:javascript
复制
// 不自定义类型也可以通过原型实现对象之间的信息共享
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}// 本质上,object()是对传入对象执行了一次浅复制
let person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};

let anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(person.friends); // Shelby, Court, Van, Rob, Barbie

ES5增加Object.create()方法将原型式继承的概念规范化。第一个参数是作为新对象原型的对象,第二个可选参数是给新对象定义额外属性的对象。

代码语言:javascript
复制
let person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};

let anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

let yetAnotherPerson =  Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(person.friends); // Shelby, Court, Van, Rob, Barbie

let p3 = Object.create(person, {
  name: {
    value: 'Mike'
  }
});
console.log(p3.name); // Mike

原型式继承适合不需要单独创建构造函数,但仍需要在对象间共享信息的场合。属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

# 寄生式继承

思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

代码语言:javascript
复制
function createAnother(original) {
  let clone = object(original); // 通过调用函数创建一个新对象
  clone.sayHi = function() { // 以某种方式增强这个对象
    console.log('hi');
  };
  return clone; // 返回这个对象
}

let person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。该方法给对象添加函数会导致函数难以重用。

# 寄生式组合继承

  • 组合继承存在效率问题,父类构造函数始终会被调用两次
    • 创建子类原型时调用
    • 在子类构造函数中调用
代码语言:javascript
复制
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name); // 第二次调用 SuperType()
  this.age = age;
}
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

  • 具体过程
    1. 定义SubType
    1. SubType.prototype = new SuperType();
    1. var instance = new SubType('Nicholas', 29);

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

代码语言:javascript
复制
function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象
  SubType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

#

ES6中引入 class 关键字,是一种语法糖,背后仍是原型和构造函数的概念。

# 类定义

代码语言:javascript
复制
// 类声明
class Person {}
// 类表达式
const Animal = class {};

  • 函数表达式和类表达式在它们被求值前不能引用
代码语言:javascript
复制
console.log(FunctionExpression); // undefined
var FunctionExpression = function() {};
console.log(FunctionExpression); // function(){}

console.log(ClassExpression); // undefined
var ClassExpression = class {};
console.log(ClassExpression); // class {}

  • 函数声明可以提升,类定义不能提升
代码语言:javascript
复制
console.log(FunctionExpression); // FunctionExpression(){}
function FunctionExpression() {}
console.log(FunctionExpression); // FunctionExpression(){}

console.log(ClassExpression); // ReferenceError: ClassExpression is not defined
class ClassExpression {}
console.log(ClassExpression); // class ClassExpression {}

  • 函数受函数作用域限制,类受块作用域限制
代码语言:javascript
复制
{
  function FunctionExpression(){}
  class ClassExpression {}
}
console.log(FunctionExpression); // FunctionExpression(){}
console.log(ClassExpression); // ReferenceError: ClassExpression is not defined

# 类的构成

类可以包含构造函数、实例方法、获取函数、设置函数和静态类方法,但都不是必需的,空的定义类也有效。建议类名大写。

代码语言:javascript
复制
// 类表达式的名称可选,可以通过name属性获取类表达式的名称字符串
let Person = class PersonName {
  identify() {
    console.log(Person.name, PersonName.name);
  }
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined

# 类构造函数

constructor关键字用于在类定义块内部创建类的构造函数,它会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义相当于将构造函数定义为空函数

# 实例化
  • 使用 new 调用类的构造函数会执行如下操作
    1. 在内存中创建一个新对象
    2. 新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性
    3. 构造函数内部的 this 被赋值为该新对象(即this指向新对象)
    4. 执行构造函数内部的代码(给新对象添加属性)
    5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的对象
代码语言:javascript
复制
class Animal {}
class Person {
  constructor() {
    console.log('person ctor');
  }
}
class Vegetable {
  constructor() {
    this.color = 'green';
  }
}
let a = new Animal; // 类实例化时传入的参数会用作构造函数的,如果不需要参数,类名后括号可选
let p = new Person(); // person ctor
let v = new Vegetable();

console.log(v.color); // orange

  • 类构造函数域构造函数的主要区别
    • 调用类构造函数必须使用new操作符,否则会抛出错误
    • 普通构造函数如果不使用new调用,就会以全局this作为内部对象
  • 类构造函数没有什么特殊之处,实例化之后会编程普通的实例方法,但仍需要使用new调用
代码语言:javascript
复制
class Person {}
// 使用类创建一个新实例
p1.constructor(); // TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();

# 把类当成特殊函数
  • ECMAScript 中没有正式的类这个类型,ECMAScript 类是一种特殊函数
代码语言:javascript
复制
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function

  • 类标识符有 prototype 属性,而 prototype 有一个 constructor 属性指向类自身
代码语言:javascript
复制
class Person {}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true

与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中

代码语言:javascript
复制
class Person {}
let p = new Person();
console.log(p instanceof Person); // true

  • 类中定义的 constructor 方法不会被当成构造函数,对其使用 instanceof 会返回false
代码语言:javascript
复制
class Person {}
let p1 = new Person();

console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false

let p2 = new Person.constructor();

console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true

  • 类是 JS 的一等公民,可以像其他对象或函数引用一样把类作为参数传递
代码语言:javascript
复制
let classList = [
  class {
    constructor(id) {
      this.id_ = id;
      console.log(`instance ${this.id_}`);
    }
  }
];
function createInstance(classDefinition, id) {
  return new classDefinition(id);
}
let foo = createInstance(classList[0], 3141); // instance 3141

  • 与立即调用函数表达式相似,类也可以立即实例化
代码语言:javascript
复制
let p = new class Foo {
  constructor(x) {
    console.log(x);
  }
}('bar'); // bar
console.log(p); // Foo{}

# 实例、原型和类成员

# 实例成员

每次通过 new 调用类标识符时,都会执行类构造函数。该函数内部,可以为新创建的实例(this)添加“自有属性”。构造函数执行完毕后,仍然可以给实例继续添加新成员。每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享。

代码语言:javascript
复制
class Person {
  constructor() {
    this.name = new String('Jack');
    this.sayName = () => console.log(this.name);
    this.nickname = ['Jake', 'J-Dog'];
  }
}
let p1 = new Person(),
    p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack

console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nickname === p2.nickname); // false

p1.name = p1.nickname[0];
p2.name = p2.nickname[0];

p1.sayName(); // Jake
p2.sayName(); // J-Dog

# 原型方法与访问器

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。

代码语言:javascript
复制
class Person {
  constructor() {
    // 添加到 this 的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance');
  }
  // 在类块中定义的所有内容都会定义在类的原型上
  locate() {
    console.log('prototype');
  }
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype

可以把方法定义在类构造函数或者类块中,但不能在类块中给原型添加添加原始值或对象作为成员数据

代码语言:javascript
复制
class Person {
  name: 'Jake'
} // Uncaught SyntaxError

类方法等同于对象属性,因此可以使用字符串、Symbol或计算的值作为键

代码语言:javascript
复制
const symbolKey = Symbol("symbolKey");
class Person {
  stringKey() {
    console.log('invoked stringKey');
  }
  [symbolKey]() {
    console.log('invoked symbolKey');
  }
  ['computed' + 'Key']() {
    console.log('invoked computedKey');
  }
}
let p = new Person();
p.stringKey(); // invoked stringKey
p[symbolKey](); // invoked symbolKey
p.computedKey(); // invoked computedKey

类定义也支持获取和设置访问器

代码语言:javascript
复制
class Person {
  set name(newName) {
    this.name_ = newName;
  }
  get name() {
    return this.name_;
  }
}
let p = new Person();
p.name = "Jane";
console.log(p.name); // Jane

# 静态类方法

可以在类上定义静态方法,这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个。静态类成员在类定义中使用 static 关键字作为前缀,在静态成员中,this引用类自身。

代码语言:javascript
复制
class Person {
  constructor() {
    // 添加到this的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance', this);
  }
  // 定义在类的原型对象上
  locate() {
    console.log('prototype', this);
  }
  // 定义在类本身上
  static locate() {
    console.log('class', this);
  }
}

let p = new Person();

p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, { constructor: .. }
Person.locate(); // class, class Person{}

  • 静态方法非常适合作为实例工厂
代码语言:javascript
复制
class Person {
  constructor(age) {
    this.age_ = age;
  }
  sayAge() {
    console.log(this.age_);
  }
  static create() {
    return new Person(Math.floor(Math.random() * 100));
  }
}
console.log(new Person()); // Person { age_: xx }

# 非函数原型和类成员

类定义不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加

代码语言:javascript
复制
class Person {
  sayName() {
    console.log(`${Person.greeting} ${this.name}`);
  }
}
// 在类上定义数据成员
Person.greeting = 'My name is ';
// 在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake

# 迭代器与生成器

类定义语法支持在原型和类本身上定义生成器方法

代码语言:javascript
复制
class Person {
  // 在原型上定义生成器方法
  *createNicknameIterator() {
    yield 'Jack';
    yield 'Jake';
    yield 'J-Dog';
  }
  // 在类上定义生成器方法
  static *createJobIterator() {
    yield 'Butcher';
    yield 'Baker';
    yield 'Candlestick maker';
  }
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); //Candlestick maker

let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog

可以通过添加一个默认迭代器,把实例变成可迭代对象

代码语言:javascript
复制
class Person {
  constructor() {
    this.nicknames = ['Jack', 'Jake', 'J-Dog'];
  }
  * [Symbol.iterator]() {
    yield *this.nicknames.entire();
  }
}
let p = new Person();
for (let [idx, nickname] of p) {
  console.log(nickname);
}

# 继承

ES6原生支持了继承机制,背后依旧使用的是原型链

# 继承基础
代码语言:javascript
复制
class Vehicle {}
// 继承类
class Bus extends Vehicle {}

let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true

function Person() {}
// 继承构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true

派生类会通过原型链访问到类和原型上定义的方法。this的值会反映调用相应方法的实例或者类

代码语言:javascript
复制
class Vehicle {
  identifyPrototype(id) {
    console.log(id, this);
  }
  static identifyClass(id) {
    console.log(id, this);
  }
}
class Bus extends Vehicle {}
let v = new Vehicle();
let b = new Bus();

b.identifyPrototype('bus'); // bus, Bus {}
v.identifyPrototype('vehicle'); // vehicle, Vehicle {}

Bus.identifyClass('bus'); // bus, class Bus {}
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}

# 构造函数、HomeObject和super()

派生类可通过super关键字引用他们的原型,该关键字只能在派生类中使用,且仅限于类构造函数、实例方法和静态方法内部。

  • 在类构造函数中使用super可以调用父类构造函数。
代码语言:javascript
复制
class Vehicle {
  consutructor() {
    this.hasEngine = true;
  }
}
class Bus extends Vehicle {
  consutructor() {
    // 不要在super()之前引用this,否则会抛出ReferenceError
    super(); // 相当于super.constructor()

    console.log(this instanceof Vehicle); // true
    console.log(this); // Bus { hasEngine: true }
  }
}
new Bus();

  • 在静态方法中可以通过super调用继承的类上定义的静态方法
代码语言:javascript
复制
class Vehicle {
  static identify() {
    console.log('vehicle');
  }
}
class Bus extends Vehicle {
  static identify() {
    super.identify();
  }
}
Bus.identify(); // vehicle

  • 注意点
    • super只能在派生类构造函数和静态方法中使用
    • 不能单独引用super关键字,要么用他调用构造函数,要么用它引用静态方法
    • 调用super()会调用父类构造函数,并将返回的实例赋值给this
    • super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
    • 如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数
    • 在类构造函数中,不能再使用super()之前引用this
    • 如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象
# 抽象基类

有时需要定义这样一个类,可供其他类继承,但本身不会被实例化。可以通过new.target实现。new.target保存通过new关键字调用的类或函数。通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化。

代码语言:javascript
复制
// 抽象基类
class Vehicle {
  constructor() {
    console.log(new.target);
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated');
    }
  }
}
// 派生类
class Bus extends Vehicle {}

new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {} 
// Error: Vehicle directly cannot be instantiated

通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前已经存在了,所以可以通过this来检查相应的方法

代码语言:javascript
复制
// 抽象基类
class Vehicle {
  constructor() {
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated');
    }
    if (!this.foo) {
      throw new Error('Inheriting class must define foo()');
    }
    console.log('success');
  }
}
// 派生类
class Bus extends Vehicle {
  foo() {}
}
// 派生类
class Van extends Vehicle {}

new Bus(); // success
new Van(); // Error: Inheriting class must define foo()

# 继承内置类型
代码语言:javascript
复制
class SuperArray extends Array {
  shuffle() {
    // 洗牌算法
    for (let i = this.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [this[i], this[j]] = [this[j], this[i]];
    }
  }
}
let a = new SuperArray(1,2,3,4,5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1,2,3,4,5]
a.shuffle();
console.log(a); // [5,2,1,3,4]

有些内置类型的方法会返回新实例,默认返回实例的类型与原始实例的类型是一致的。如果想覆盖这个默认行为,则可以覆盖Symbol.species访问器,该访问器决定在创建返回的实例时使用的类

# 类混入

把不同类的行为集中到一个类是一种常见JS模式

代码语言:javascript
复制
// extends 后面可以跟JS表达式,只要可以解析为一个类或者构造函数就行
class Vehicle {}
function getParentClass() {
  console.log('evaluated expression');
  return Vehicle;
}
class Bus extends getParentClass() {} // 可求值表达式

混入模式可以通过在一个表达式中连缀多个混入元素类实现,这个表达式最终会解析为一个可以被继承的类

代码语言:javascript
复制
// 可以通过定义一组可嵌套函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
  foo() {
    console.log('foo');
  }
};
let BarMixin = (Superclass) => class extends Superclass {
  bar() {
    console.log('bar');
  }
};
let BazMixin = (Superclass) => class extends Superclass {
  baz() {
    console.log('baz');
  }
};
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

  • 可以通过辅助函数,将嵌套调用展开
代码语言:javascript
复制
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
  foo() {
    console.log('foo');
  }
};
let BarMixin = (Superclass) => class extends Superclass {
  bar() {
    console.log('bar');
  }
};
let BazMixin = (Superclass) => class extends Superclass {
  baz() {
    console.log('baz');
  }
}
function mix(BaseClass, ...Mixins) {
  return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

很多JS框架(特别的React)已经抛弃混入模式,转向组合模式

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020/12/3,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 理解对象
    • # 属性的类型
      • # 数据属性
      • # 访问器属性
    • # 定义多个属性
      • # 读取属性的特性
        • # 合并对象
          • # 对象标识及相等判定
            • # 增强对象语法
              • # 对象解构
              • # 创建对象
                • # 工厂模式
                  • # 构造函数模式
                    • # 原型模式
                      • # 对象迭代
                        • # 其他原型语法
                        • # 原型的动态性
                        • # 原生对象原型
                        • # 原型的问题
                    • # 继承
                      • # 原型链
                        • # 默认原型
                        • # 原型与继承关系
                        • # 关于方法
                        • # 原型链的问题
                      • # 盗用构造函数
                        • # 传递参数
                        • # 盗用构造函数
                      • # 组合继承
                        • # 原型式继承
                          • # 寄生式继承
                            • # 寄生式组合继承
                            • # 类
                              • # 类定义
                                • # 类的构成
                              • # 类构造函数
                                • # 实例化
                                • # 把类当成特殊函数
                              • # 实例、原型和类成员
                                • # 实例成员
                                • # 原型方法与访问器
                                • # 静态类方法
                                • # 非函数原型和类成员
                                • # 迭代器与生成器
                              • # 继承
                                • # 继承基础
                                • # 构造函数、HomeObject和super()
                                • # 抽象基类
                                • # 继承内置类型
                                • # 类混入
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档