- 对象:一组属性的无序集合,对象的每个属性或方法都由一个映射到一个值对名称来标识。
- 想成一个散列表,内容就是一组名/值对,值可以是数据或函数。
1、理解对象
-
创建自定义对象,创建Object实例。
let person = new Object() person.name = "Feng" person.age = 25 person.sayNam = function() { console.log(this.name) }
-
对象字面量创建
let person = { name: "Feng", age: 25, sayName() { console.log(this.name); } };
1、属性的类型
- 一些内部特性来描述属性的特征,为js实现引擎规范定义的。
- 开发者不能直接访问,通常用两个中括号扩起来。
1、数据属性
包含一个保存数据值的位置,值会从这个位置读写。
-
[[Configurable]]
- 是否可以通过delete删除
- 是否可以修改它的特性
- 是否可以把它改为访问器属性
- 默认直接定义的是true
-
[[Enumerable]]
- 是否可以通过for-in循环返回
- 默认直接定义的是true
-
[[Writable]]
- 是否可以修改属性的值
- 默认直接定义的是true
-
[[Value]]
- 包含属性实际的值,读写属性值的位置
- 默认undefined
-
修改属性默认特征,必须使用
Object.defineProperty()
- 要给其添加属性的对象
- 属性的名称
- 描述符对象 (不设置都是false)
- configureable (一旦设置为false,不能再改回来)
- enumerable
- writable
- value
2、访问器属性
-
包含一个获取getter函数和设置setter函数。
-
[[Configurable]]
- 是否可以通过delete删除并重新定义
- 是否可以修改它的特性
- 是否可以把它改为数据属性
- 默认直接定义的是true
-
[[Enmuerable]]
- 是否可以通过for-in循环返回
- 默认直接定义的是true
-
[[Get]]
- 获取函数,在读取属性的时候调用
- 默认undefined
-
[[Set]]
- 设置函数,在写入属性的时候调用
- 默认undefined
-
访问器属性不能直接定义,必须使用
Object.defineProperty()
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 } } })
- 只定义获取函数
- 意味着属性只读
- 修改会忽略
- 严格模式报错。
- 只定义设置函数
- 意味着属性只写
- 读取会返回undefined
- 严格模式报错
- 只定义获取函数
2、定义多个属性
- 同时定义多个属性
Object.defineProperties
- 要添加或修改属性的对象
- 描述符对象,其属性和要添加或修改的属性一一对应
3、读取属性的特征
- 方法
Object.getOwnPropertyDescriptor
可以取得制定属性的属性描述符。- 属性所在的对象
- 要取得其描述符的属性名
- 返回对象
- 访问属性包含configurable,enumerable,get,set
- 数据属性包含configurable,enumerable、writabel、value
Object.getOwnPropertyDescriptors()
,实际在每个自有属性上调用了Object.getOwnPropertyDescriptor()
,并在新对象返回
4、合并对象
-
把源对象所有的本地属性一起复制到目标对象上
-
方法
Object.assign()
- 接收目标对象和一个或多个源对象作为参数
- 将每个源对象中的可枚举(
Object.propertyIsEnumerable()
返回true)和自有(Object.hasOwnProperty()
返回 true)属性复制到目标对象。 - 以字符串或符号为键的属性会被复制。
- 对符合条件的属性,这个方法会使用源对象上的
[[Get]]
取得属性值,然后使用目标对象上的[[Set]]
设置属性的值
/** * 获取函数与设置函数 */ let dest, src dest = { set a(val) { console.log(`Invoked dest setter with param ${val}`); } }; src = { get a() { console.log('Invoked src getter'); return 'foo'; } }; Object.assign(dest, src); // 调用 src 的获取方法 // 调用 dest 的设置方法并传入参数"foo" // 因为这里的设置函数不执行赋值操作 // 所以实际上并没有把值转移过来 console.log(dest); // { set a(val) {...} }
-
Object.assign()
实际上对每个源对象执行的是浅复制 -
如果多个源对象都有相同的属性,则使用最后一个复制的值
-
从源对象访问属性取得多值,会作为一个静态值复制给目标对象,即不能在两个对象间转移获取函数和设置函数。
let dest, src /** * 对象引用 */ dest = {}; src = { a: {} }; Object.assign(dest, src); // 浅复制意味着只会复制对象的引用 console.log(dest); // { a :{} } console.log(dest.a === src.a); // true
-
如果赋值期间出错,则操作会终止并退出,抛出错误。
-
Object.assign()
没有回滚之前赋值概念,可能只会部分复制let dest, src; /** * 错误处理 */ dest = {}; src = { a: 'foo', get b() { // Object.assign()在调用这个获取函数时会抛出错误 throw new Error(); }, c: 'bar' }; try { Object.assign(dest, src); } catch(e) {} // Object.assign()没办法回滚已经完成的修改 // 因此在抛出错误之前,目标对象上已经完成的修改会继续存在: console.log(dest); // { a: foo }
5、对象标识及相等判定
-
ES6之前,有些特殊情况 === 无能为力
// 合理情况 true === 1 // false {} === {} // false "2" === 2 // false // JS引擎中表现不同,但认为相等 +0 === -0 // true +0 === 0 // true -0 === 0 // true // NaN 需要用isNaN() NaN === NaN // false
-
ES6新增
Object.is()
Object.is(true, 1) // false Object.is({}, {}) // false Object.is("2", 2) // false Object.is(+0, -0) // false Object.is(+0, 0) // true Object.is(-0, 0) // false Object.is(NaN, NaN) // true
-
检查超过两个值,递归利用相等性
function recursiveCheckEqual(x, ...rest){ return Object.is(x, rest[0]) && (rest.length < 2 || recursiveCheckEqual(...rest)) }
6、增强的对象语法
- 为定义和操作对象新增了很多语法糖特征,提升了处理对象的方便程度
- 同样适用于ES6中的类
1、属性值简写
-
属性名和变量名一样,只要写变量名。
let name = "Feng" let person = { name }
2、可计算属性
-
在此之前,如果想要变量的值作为属性,必须先声明对象,然后使用中括号语法添加属性。
-
有了可计算属性,可以在对象字面量中完成动态属性赋值
const nameKey = 'name'; const ageKey = 'age'; const jobKey = 'job'; // old let person = {}; person[nameKey] = 'Matt'; person[ageKey] = 27; person[jobKey] = 'Software engineer'; // new let person = { [nameKey]: "Matt", [ageKey]: 27, [jobKey]: "Software enegineer" }
-
可计算属性本身可以是复杂表达式,在实例化时再求值
const nameKey = 'name'; const ageKey = 'age'; const jobKey = 'job'; let uniqueToken = 0; function getUniqueKey(key) { return `${key}_${uniqueToken++}`; } let person = { [getUniqueKey(nameKey)]: 'Matt', [getUniqueKey(ageKey)]: 27, [getUniqueKey(jobKey)]: 'Software engineer' }; console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
-
可计算属性表达式抛出错误会中断对象创建,之前的计算不能回滚
3、简写方法名
-
放弃给函数表达式命名
// old let person = { sayName: function(name) { console.log(`My name is ${name}`); } }; // new let person = { sayName(name) { console.log(`My name is ${name}`); } };
-
对set,get同样适用
-
可以于计算属性键兼容
const methodKey = 'sayName'; let person = { [methodKey](name) { console.log(`My name is ${name}`); } }
7、对象解构
-
对象解构,可以在一条语句中使用嵌套数据实现一个或多个赋值操作
-
使用与对象匹配的解构来实现对象属性赋值
let person = { name: 'Matt', age: 27 }; // 不使用对象解构 let personName = person.name, personAge = person.age; console.log(personName); // Matt console.log(personAge); // 27 // 使用对象解构 let { name: personName, age: personAge } = person; console.log(personName); // Matt console.log(personAge); // 27
-
如果想让变量直接使用属性的名称,那么可以使用简写语法
let { name, age } = person; console.log(name); // Matt console.log(age); // 27
-
解构赋值不一定与对象属性匹配,可以忽略某些属性,如果引用属性不存在则为undefined
let {name, job} = person console.log(name); // Matt console.log(job); // undefined
-
解构时可以定义默认值
let {name, job = 'teacher'} = person console.log(name); // Matt console.log(job); // teacher
-
解构在内部调用ToObject() 把源数据结构转为对象。意味着原始值会被当成对象,也意味着null和undefined不能被解构
let { length } = 'foobar' length // 6 let {constructor: c} = 4 c === Number // true let { _ } = null // TypeError let { _ } = undefined // TypeError
-
不要求变量必须在解构表达式中声明,但若给实现声明变量赋值,则赋值表达式必须包含在一对括号里。
let personName, personAge let person = { name: 'Matt', age: 27 }; ({name: personName, age: personAge} = person)
1、嵌套解构
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
-
解构对与引用嵌套的属性或赋值目标没有限制,可用来赋值对象属性
let personCopy = {}; ({ name: personCopy.name, age: personCopy.age, job: personCopy.job } = person); // 因为一个对象的引用被赋值给 personCopy,所以修改 // person.job 对象的属性也会影响 personCopy person.job.title = 'Hacker' console.log(person); // { name: 'Matt', age: 27, job: { title: 'Hacker' } } console.log(personCopy); // { name: 'Matt', age: 27, job: { title: 'Hacker' } }
-
解构赋值也可以用嵌套结构
// 声明title变量并将person.job.title的值赋给它 let {job: {title}} = person console.log(title) // Software engineer
-
在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象
2、部分解构
-
涉及多个属性的解构赋值是一个输出无关的顺序化操作
-
如果开始赋值成功而后面出错,则只会完成一部分
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
3、参数上下文匹配
-
函数的参数列表也可以解构赋值
-
不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量。
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 printPerson2('1st', person, '2nd'); // ['1st', { name: 'Matt', age: 27 }, '2nd'] // 'Matt', 27
2、创建对象
- 创建具有同样接口的多个对象需要重复代码
1、概述
- ES6开始正式支持类和继承,ES5可以通过运用原型链继承模拟
2、工厂模式
-
用于抽象创建特定对象的过程
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 person1 = createPerson("Jack", 14, "student") let person2 = createPerson("Mick", 32, "teacher")
- 没有解决对象标识问题(新创建的对象是什么类型)
3、构造函数模式
-
自定义构造函数,以函数的形式为自己的对象类型定义属性方法
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("Jack", 14, "student") let person2 = new Person("Mick", 32, "teacher") person1.sayName() // Jack person1.sayName() // Mick
-
Person() 构造函数替代了createPerson() 工厂函数
-
Person()内部代码和createPerson()基本一致 除了
- 没有显示创建对象
- 属性和方法赋值给了this
- 没有return
-
Person() 构造函数首字母大写
-
创建Person实例要用new操作符,实际执行了
- 在内存中创建一个新对象
- 新对象内部[[Prototype]]赋值为构造函数prototype属性
- 构造函数内部的this被赋值给这个新对象(this指向新对象)
- 执行构造函数内部代码(添加属性)
- 如果构造函数返回非空对象则返回该对象,否则返回刚创建的对象。
-
实例的constructor属性指向构造函数
person1.constructor === Person // true person2.constructor === Person // true
-
constructor是标识对象类型的,但instanceof是更可靠方式
person1 instanceof Person //true person1 instanceof Object //true
-
-
构造函数不一定要写成函数声明的模式
let Person = function(name, age ,job) { ... } ...
-
实例化时没如果不想传参数,那么构造函数后面括号可以省略
let person2 = new Person
1、构造函数也是函数
-
唯一区别就是调用方式不同。任何函数只要使用new操作符就是构造函数,不使用就是普通函数。
// 作为构造函数 let 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"
- 在调用一个函数没有明确设置this的情况下(即没有作为对象的方法调用,或者没有使用call/apply调用),this始终指向Global对象(浏览器window对象)。
- 通过call() 调用函数将特定对象制定为作用域。
2、构造函数的问题
-
定义的方法会在每个实例上都创建一遍,即不同实例的相同方法不是同一个Function实例。每次定义函数的时候都会初始化一个对象,实际相当于这样的:
function Person(name, age, job) { this.name = name this.age = age this.job = job this.sayName = new Function("console.log(this.name)") }
- 不同实例上的函数虽然同名但并不相等
person1.sayName == person2.sayName // false
-
可以通过把函数定义转移到构造函数外部解决这个问题
function Person(name, age, job) { this.name = name this.age = age this.job = job this.sayName = sayName } function sayName() { console.log() }
- 缺点:全局作用域混乱了,可以通过原型模式解决
4、原型模式
-
每个函数会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
-
prototype这个对象就是通过调用构造函数创建的对象的原型。
-
在prototype上面定义的属性和方法可以被对象实例共享。
-
原来在构造函数上直接赋值给对象实例等值,可以直接赋值给它们的原型prototype
function Person() {} // 或 let Person = function() {} Person.prototype.name = "Nike" Person.prototype.age = 18 Person.prototype.job = "student" Person.prototype.sayName = function() { console.log(this.name) } let person1 = new Person() let person2 = new Person() person1.sayName() // Nike person2.sayName() // Nike person1.sayName === person2.sayName
1、理解原型
-
只要创建一个函数,就会按特定规则为该函数创建
prototype
属性(指向原型对象)。 -
所有原型对象自动获得一个名为
constructor
的属性,指回与之关联的构造函数。Person.prototype.constructor === Person // true
-
因构造函数而异,可以给原型对象添加其他属性方法
-
定义构造函数的时候,原型对象默认只会获得
constructor
属性,其他方法都继承自Object
。 -
每次调用构造函数创建新实例,这个实例内部
[[Prototype]]
指针会被赋值为构造函数的原型对象,这个就是__proto__
,通过这个属性可以访问对象的原型 -
实例与构造函数原型有直接联系,但与构造函数之间没有。
person1.__proto__ === Person.prototype
function Person() {} // 声明之后,构造函数就有了一个与之关联的原型对象 typeof Person.prototype // object Person.prototype //{ // constructor: f Person(), // __proto__: Object //} // 构造函数的prototype属性引用其原型对象 // 这个原型对象也有一个constructor属性引用这个构造函数 // 所以两者循环引用 Person.prototype.constructor === Person // true // 正常原型链都会终止于Object的原型对象 // Object原型的原型是null Person.prototype.__proto__ === Object.prototype Person.prototype.__proto__.constructor === Object Person.prototype.__proto__.__proto__ === null Person.prototype.__proto__ //{ // constructor: f Object(), // toString: ... // hasOwnProperty: ... // isPrototypeOf: ... // ... //} let person1 = new Person() let perosn2 = new Person() person1 !== Person person1 !== Person.prototype Person.prototype !== Person // 实例可以通过__proto__ 链接到原型对象 person1.__proto__ === Person.prototype person1.__proto__.constructor === Person // 同一个构造函数的两个实例共享一个原型对象 person1.__proto__ === person2.__proto__ person1 instanceof Person person1 instanceof Object Person.prototype instanceof Object
-
isPrototypeOf() 确定两个对象之间的
__proto__
关系Person.prototype.isPrototypeOf(person1) // true Person.prototype.isPrototypeOf(person2) // true // 即 person1.__proto__ === Person.prototype // 即 person2.__proto__ === Person.prototype
-
Object.getPrototypeOf()
返回参数的内部特性[[Prototype]]
Object.getPrototypeOf(person1) === Person.prototype Object.getPrototypeOf(person1).name // Nike
-
Object.setPrototypeOf
可以向私有特性[[Prototype]]
写入一个新值。可以重写对象原型继承关系let biped = { numLegs: 2 } let person = { name: "Matt" } Object.setPrototypeOf(person, biped) person.name // "Matt" person.numLegs // 2 Object.getPrototypeOf(person) === biped
- 严重影响性能,严重影响继承关系,避免使用
-
Object.create()
创建新对象,同时指定原型let biped = { numLegs: 2 } let person = Object.create(biped) person.name = "Matt" person.name // "Matt" person.numLegs // 2 Object.getPrototypeOf(person) === biped
2、原型层级
-
通过对象访问属性,会按照这个属性的名称开始搜索。
-
从这个实例本身开始搜索,没找到的时候会沿着指针进入原型对象
-
constructor
属性只存在于原型对象中,所以通过实例对象也可以访问到的。 -
虽然可以通过实例读取到原型对象的值,但不可能通过实例去重写这些值。如果添加一个与原型对象同名属性,那么会在实例上创建这个属性,这个属性会遮住原型对象上的属性
function Person() {} Person.prototype.name = "Nicholas" Person.prototype.age = 29 Person.prototype.job = "Software Engineer" Person.prototype.sayName = function() { console.log(this.name) } let person1 = new Person() let person2 = new Person() person1.name = "Grace" perosn1.name // "Grace" 来自实例 person2.name // "Nicholas" 来自原型 // 即使把实例上这个属性设置为null,也无法恢复它和原型的联系 // 不过可以通过delete操作完全删除实例上的这个属性 delete person1.name person1.name // "Nicholas" 来自原型
-
hasOwnProperty()
方法用于确定某个属性是在实例上还是在原型对象上。该方法继承自Object
的,会在属性存在于调用它的对象实例上返回true。let person1 = new Person() person1.hasOwnProperty("name") // false person1.name = "Franck" person1.hasOwnProperty("name") // true delete person1.name person1.hasOwnProperty("name") // false
-
Object.getOwnPropertyDescriptor
只对实例属性有效,若想获取原型属性的描述符,必须直接在原型对象上调用。
3、原型和in操作符
-
in
操作符for-in
循环- 单独使用
- 可以通过对象访问指定属性时返回true
- 无论在实例上还是在原型上
let person1 = new Person() person1.hasOwnProperty("name") // false "name" in person1 // true person1.name = "Greg" person1.hasOwnProperty("name") // true "name" in person1 // true delete person1.name person1.hasOwnProperty("name") // false "name" in person1 // true
-
确定某个属性是否仅存在于原型上
function hasOwnPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && (name in object) } let person = new Person() hasOwnPrototypeProperty(person, "name") // true person.name = "Greg" hasOwnPrototypeProperty(person, "name") // false
-
for-in
循环使用in,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举([[Enmuerable]]特征被设置为false)属性的实例属性也会在for-in循环返回。 -
通过
Object.keys()
方法获得对象上所有可枚举的实例属性- 接收一个对象作为参数
- 返回包含该对象所有可枚举属性名称的字符串数组
Object.keys(Person.prototype) // ["name","age","job","sayName"] let p1 = new Person() p1.name = "Rob" p1.age = 31 Object.keys(p1) // ["name", "age"]
-
Object.getOwnPropertyNames
所有实例属性,无论是否可枚举使用Object.getOwnPropertyNames(Person.prototype) // ["constructor", "name", "age", "job", "sayName"]
-
Object.getOwnPropertySymbols
仅针对Symbol符号let k1 = Symbol('k1') let k2 = Symbol('k2') let o = { [k1]: 'k1', [k2]: 'k2' } Object.getOwnPropertySymbols(o) // [Symbol(k1),Symbol(k2)]
4、属性枚举循环
-
for-in
- 枚举顺序不确定
- 返回可以被枚举的实例属性和原型属性都会
-
Object.keys()
- 枚举顺序不确定
- 返回可以被枚举的实例属性
-
Object.getOwnPropertyNames()
- 顺序确定
- 先以升序枚举数值键
- 然后以插入顺序枚举字符串和符号键
- 列出所有非符号实例属性,无论是否可枚举使用
- 顺序确定
-
Object.getOwnPropertySymbols()
- 顺序确定
- 先以升序枚举数值键
- 然后以插入顺序枚举字符串和符号键
- 列出所有符号实例属性,无论是否可枚举使用
- 顺序确定
-
Object.assign()
- 顺序确定
- 先以升序枚举数值键
- 然后以插入顺序枚举字符串和符号键
let k1 = Symbol('k1'), k2 = Symbol('k2'); let o = { 1: 1, first: 'first', [k1]: 'sym2', 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)]
- 顺序确定
5、对象迭代
-
Object.values()
- 返回对象值的数组
-
Object.entries()
- 返回键/值对的数组
const o = { foo: "bar", baz: 1, qux: {} } Object.values(o) // ["bar", 1, {}] Object.entries(o) // [["foo","bar"],["baz",1],["qux",{}]]
-
非字符串属性会被转换为字符串输出,执行浅复制
const o = { qux: {} } Object.values(o)[0] === o.qux Object.entries(o)[0][1] === o.qux
-
符号属性会被忽略
const sym = Symbol() const o = { [sym]: "foo" } Object.values(o) // [] Object.entries(o) // []
1、其他原型方法
-
字面量方法重写原型
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }
-
问题 重写后Person.prototype的constructor属性不能指向Person
let friend = new Person() friend instance of Object // true friend instance of Person // true friend.constructor === Person //false friend.constructor === Object //true
-
这样重写,[[Enmuerable]]为true
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }
Object.defineProperty(Person.prptotype, "constructor",{ enumerable: false, value: Person })
-
2、原型的动态性
-
从原型上搜索值是动态的,即使实例在原型修改前已经存在,任何时候对原型对象的修改也会在实例上反应出来。
let friend = new Person() Person.prototype.sayHi = function() { console.log('hi') } friend.sayHi() // hi
-
虽然friend实例在添加方法后面创建的,但仍可访问。
- 实例和原型之间松散的联系,所以会搜索原型对象。
- 实例和原型之间就是简单的指针而不是保存副本,所以可以找到这个函数。
-
但如果是重写整个原型
- 实例的
[[Prototype]]
指针是在调用构造函数的时候自动赋值的 ,即使把原型修改成不同的对象也不会变 - 重写整个原型会切断最初原型与构造函数的联系。
- 实例仍然引用最初原型,实例只有指向原型的指针,而没有指向构造函数的指针
function Person() {} let friend = new Person(); Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }; friend.sayName(); // 错误
- 实例的
-
3、原生对象原型
-
原型模式也是实现所有原生引用类型的模式
-
所有原生引用类型(包括Object,Array,String等)都在原型上定义了实例方法
typeof Array.prototype.sort // function typeof String.prototype.substring // function
-
可以给原生类型定义新方法
-
不推荐在产品环境修改原生对象原型,可能造成意外。
-
推荐创建一个自定义的类继承原生类型
4、原型的问题
-
弱化了向构造函数传递初始化参数的能力,导致所有实例默认取得相同的属性值。
-
共享特征,原型上所有属性都是在实例间共享的,对于包含引用值的属性会有问题
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", friends: ["Shelby", "Court"], sayName() { console.log(this.name); } }; let person1 = new Person(); let person2 = new Person(); person1.friends.push("Van"); person1.friends // ["Shelby", "Court", "Van"] person2.friends // ["Shelby", "Court", "Van"] person1.friends === person2.friends // true
- 由于friends存在Perosn.prototype而非person1上,新加的内容也会在person2.friends上反映出来
- 影响了自己属性副本的独立性。这有是不单独使用原型模式原因
3、继承
- 其他语言:
- 接口继承(只继承方法签名,JS不可能)
- 实现继承(继承实际的方法)
1、原型链
-
主要的继承方式,通过原型链继承多个引用类型的属性和方法
-
基本构想
- 每个构造函数都有一个原型对象
- 原型有一个属性指向构造函数
- 实例有一个内部指针指向原型
- 原型又是另一个类型的实例
- 原型本身有一个内部指针指向另一个原型
- 另一个原型也有一个属性指向另一个构造函数
-
实现原型链:
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() instance.getSuperValue() // true
- SubType 通过 创建 SuperType的实例并将其赋值给自己的原型SubType.prototype实现了对SuperType的继承
- 重写了SubType最初的原型替换为SuperType实例
- SuperType实例可以访问的所有属性和方法也存在于SubType.prototype
- 又给SubType.prototype即SuperType的实例添加新方法
- 创建了SubType的实例instance
- instance.getSuperValue() 搜索了三次 才找到这个方法。
1、默认原型
- 默认情况下,所有引用类型都继承自Object,任何函数默认原型都是一个Object实例
- 实例有一个内部指针指向Object.prototype, 因此自定义类型能继承包含toString()、valueOf() 在内的所有默认方法。
2、原型与继承的关系
-
instanceof
- 如果一个实例的原型链中出现过相应的构造函数则返回true
instance instanceof Object instance instanceof SuperType instance instanceof SubType
-
isPrototypeOf()
- 原型链中每个原型都可以调用这个方法,只要原型链中包含这个属性就返回true
Object.prototype.isPrototypeOf(instance) SuperType.prototype.isPrototypeOf(instance) SubType.prototype.isPrototypeOf(instance)
3、关于方法
-
子类需要覆盖父类方法或者增加父类方法
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 }
-
对象字面量方式创建原型方法会破坏之前的原型链,相当于重写了原型链
// 继承SuperType SubType.prototype = new SuperType() // 新方法 SubType.prototype = { getSubValue() { return this.subproperty }, someOtherMethod() { return false } } let instance = new SubType() instance.getSuperValue() // 报错!!
4、原型链问题
- 原型中包含引用值的时候会在所有实例共享,所以属性经常会在构造函数定义而不会定义在原型上
- 使用原型实现继承时,原型实际上变成了另一个类型的实例,即原先的实例属性摇身一变成了原型属性。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
instance1.colors // "red,blue,green,black"
let instance2 = new SubType();
instance2.colors // "red,blue,green,black"
- 子类型在实例化时不能给父类型构造函数传参。
- 无法在不影响所有对象实例的情况下把参数传给父类的构造函数。同时原型中包含引用值问题,则原型链不会单独使用
2、盗用构造函数
-
目的是解决原型包含引用值导致的继承问题。
-
又称“对象伪装”或“经典继承”
-
思路:
- 子类构造函数中调用父类构造函数
- 函数就是在特定上下文中执行代码的简单对象,用apply() 和 call() 方法以新创建的对象为上下文执行构造函数
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { SuperType.call(this) } let instance1 = new SubType(); instance1.colors.push("black"); instance1.colors // "red,blue,green,black" let instance2 = new SubType(); instance2.colors // "red,blue,green"
- 通过使用call或apply,SuperType在构造函数为SubType的实例创建新对象的上下文执行了。
1、传递参数
function SuperType(name) {
this.name = name
}
function SubType() {
// 继承并传参
SuperType.call(this, "Feng")
// 实例属性
this.age = 29
}
let instance = new SubType()
instance.name // "Feng"
instance.age // 29
2、盗用构造函数问题
- 必须在构造函数中定义方法,因此函数不能重用
- 子类不能访问父类原型上的方法,所有类型只能使用构造函数模式。
3、组合继承
-
组合继承(也称伪经典继承)
-
综合了原型链和盗用构造函数,集中了两者优点。
-
使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。
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("Feng", 25) instance1.colors.push("black") instance1.colors //["red","blue","green","black"] instance1.sayName() // "Feng" instacn1.sayAge() // 25 let instance2 = new SubType("Zhu", 23) instance1.colors //["red","blue","green"] instance1.sayName() // "Zhu" instacn1.sayAge() // 23
- SuperType 构造函数定义了两个属性,原型上定义了一个方法
- SubType构造函数调用了SuperType构造函数,又定义了自己的属性
- SubType.prototype也被赋值为SuperType实例
- 这个原型上添加了新方法,创建的实例有自己的属性,同时共享相同方法
-
弥补了原型链和盗用构造函数不足,使用最多。
-
同时保留了instanceof操作符和isPrototypeOf() 方法识别合成对象能力
4、原型式继承
-
即使不自定义类型也可通过原型实现对象间的信息共享
function object(o) { function F() {} F.prototype = o return new F() }
- 创建一个临时构造函数,将传入的对象复制给这个构造函数原型
- 返回这个临时类型的一个实例。
- 本质上是进行了一次浅复制
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"
-
在一个对象的基础上创建一个新对象,需要把这个对象先传给object,然后再对返回的对象进行适当修改。
-
原型式继承概念规范化 -->
Object.create()
- 参数一,作为新对象原型的对象
- 参数二,给新对象定义额外属性的对象(可选)
- 只有一个参数等同于object方法
- 第二个参数与
Object.definePrototype()
第二个参数一样- 每个新增属性都通过各自的描述符描述
- 这种方式添加的属性会遮蔽原型对象上的同名属性
-
非常适合不需要单独创建构造函数,但仍需在对象间共享信息的场合
-
属性中包含的引用值始终会在相关对象间共享,跟使用原型模式一样
5、寄生式继承
-
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original) { let clone = object(original) // 通过调研函数创建一个对象 clone.sayHi = function() { // 以某种方式增强这个对象 console.log('hi') } return clone // 返回这个对象 }
-
适合主要关注对象而不在乎类型和构造函数场景
-
通过寄生式继承给对象添加函数导致函数难以重用,类似构造函数模式。
6、寄生式组合继承
-
组合继承存在效率问题
-
父类构造函数始终会被调用两次
- 创建子类原型时
- 子类构造函数中调用
-
本质上子类原型最终要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己原型就行了。
function SuperType(name) { this.name = name this.color = ['red','green','blue'] } 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.constructor = SubType SubType.prototype.sayAge = function() { console.log(this.age) }
-
-
寄生组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。
-
基本思路
- 不通过调用父类构造函数给子类原型赋值,而是取得父类原型的副本
- 使用寄生式继承来继承父类原型,将返回的新对象赋值给子类原型
function object(o) { function F() {} F.prototype = o return new F(); } function inheritPrototype(subType, superType) { let prototype = object(superType.prototype) // 创建对象 prototype.constructor = subType // 增强对象 subType.prototype = prototype // 赋值对象 }
-
实现了寄生式组合继承的核心逻辑
- 先创建父类原型的副本
- 给返回的prototype对象设置constructor属性,解决重写原型导致默认constructor丢失问题。
- 将新创建的对象赋值给子类型的原型。
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) }
- 只调用了一次SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性
- 效率更高,原型链保持不变,instanceof操作符和isPrototypeOf() 方法正常有效,最佳模式。
4、类
- 实际上是语法糖结构,背后仍然是原型和构造函数
1、类定义
-
类声明
class Person{}
-
类表达式
const Animal = class {}
-
与函数的区别
-
函数声明可以提升,但类定义不能
Person // 报错 class Person {} Dog // undefined var Dog = class {} Animal // Animal() {} function Animal() {} Pig // undefined var Pig = function() {}
-
函数受函数作用域限制,类受块作用域限制
{ function FunDeclaration(){} class ClassDeclaration{} } FunDeclaration // FunDeclaration() {} ClassDeclaration // 报错
-
-
类的构成
-
包含构造函数方法、实例方法、获取函数、设置函数和静态类方法
-
类名首字母大写以区别于创建的实例
-
类表达式名称可选,表达式复制给变量后可以通过name属性获得类表达式名称字符串,但不能在类表达式作用域外访问这个标志符
let Person = class PersonName { print() { console.log(Person.name, PersonName.name) } } let p = new person() p.print() // PersonName PersonName Person.name // PersonName PersonName // 报错
-
2、类构造函数
- 不定义相当于空的构造函数
1、实例化
-
使用new操作符实例化Person的操作等于使用new调用其构造函数
- 在内存中创建一个新对象
- 这个对象内部 [[Prototype]]指针被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(this指向新对象)
- 执行构造函数内部代码(给新对象添加属性)
- 如果构造函数返回非空对象则返回该对象否则返回新创建的对象
-
不传参数的时候,括号可选
-
默认返回this对象,如果返回其他对象,这个对象不会通过instanceof操作符检测出跟类有关联,因为这个类的原型指针没修改
class Person { constructor(override) { this.foo = 'foo' if(override) { return { bar: 'bar' } } } } let p1 = new Person() let p2 = new Person(true) p1 // Perosn{foo: 'foo'} p1 instanceof Person // true p2 // {bar: 'bar'} p2 instanceof Person //false
-
类构造函数必须使用new操作符,普通构造函数不用new的时候会以全局this作为内部对象
function Person() {} class Animal {} // window 作为this来构建实例 let p = Person() let a = Animal() //报错
-
没有特别之处,实例化后就是普通的实例方法,但作为类构造函数,仍要用new调用,实例化之后可以在实例上引用它
class Person {} // 使用类创建一个新实例 let p1 = new Person() p1.constructor() // 报错 // 使用对类构造函数的引用创建一个新实例 let p2 = new p1.constructor()
2、把类当成特殊函数
-
可以用typeof操作符检测是函数
class Person {} typeof Person // functon
-
类标志符有prototype属性,而这个原型也有一个constructor属性指向类自身
class Person {} Person.prototype // {constructor: f()} Person === Person.prototype.constructor
-
可以使用instanceof操作符检查构造函数原型是否存在于实例原型链中
class Person {} let p = new Person() p instanceof Person // true
-
类的上下文中,类本身在使用new调用时就会被当成构造函数
- 类中定义的constructor不会被当作构造函数,对其使用instanceof返回false。
- 如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么instanceof操作符的返回值会反转
class Person{} let p1 = new Person() p1.constructor === Person //true[实际是__proto__下] p1 instanceof Person // true p1 instanceof Person.constructor // false let p2 = new Person.conctructor() p2.constructor === Person // false p2 instanceof Person // false p2 instanceof Person.constructor // true
-
可以像其他对象或函数引用一样把类作为参数传递
-
与立即调用函数表达式类似,类可以立即实例化
let p = new Class Foo { constructor(x) { console.log(x) } }('bar') // bar console.log(p) // Foo {}
3、实例、原型和类成员
1、实例成员
-
在构造函数内部,可以为新创建的实例(this)添加“自有”属性。
-
添加什么样的属性没有限制
-
构造函数执行完毕后,仍可以给实例添加新成员
-
每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享。
class Person { constructor() { this.name = new String('Jack') this.sayName = ()=> console.log(this.name) this.nicknames = ['Jake', 'J-Dog'] } } let p1 = new Person() let p2 = new Person() p1.sayName() //Jack p2.sayName() //Jack p1.name === p2.name //false p1.sayName === p2.sayName //false p1.nicknames === p2.nicknames //false p1.name = p1.nicknames[0] p2.name = p2.nicknames[1] p1.sayName() // Jake p2.sayName() // J-Dog
2、原型方法与访问器
-
为了在实例间共享方法,类定义语法在类块内定义的方法作为原型方法
class Person { constructor() { // 添加this的所有内容都会存在于不同实例上 this.locate = () => console.log('instance') } locate() { console.log('prototype') } } let p = new Person() p.locate() // instance Person.prototype.locate() //prototype // p.__proto__ === Person.prototype
-
可以把方法定义在类构造函数或类块中,但不能在类块内给原型添加原始值或对像作为成员数据
class Person { name: 'Jack' } // 报错
-
类方法等同于对象属性,可以使用字符串、符号或计算的值作为键
const symbolKey = Symbol('symbolKey') class Person { stringKey() { } [symbolKey]() { } ['computed' + 'Key']() { } } let p = new Person() p.stringKey() p[symbolKey]() p.computedKey()
-
类定义也支持设置和获取访问器,和普通对象一样
class Person { set name(newName) { this._name = newName } get name() { return this._name } }
3、静态类方法
-
用于执行不特定于实例的方法
-
不要求存在类的实例
-
static作为前缀,静态成员中this引用类本身
class Person { constructor() { 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{}
-
静态类方法适合作为实例工厂
4、非函数原型和类成员
-
类定义并不显式支持在原型或类上添加成员数据,但在类定义外部可以手动添加。
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
-
类定义中之所以没有显式支持添加数据成员,因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般对象实例应该独自拥有通过this引用的数据
5、迭代器与生成器方法
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog'
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker'
}
}
let jobIter = Person.createJobIterator()
jobIter.next().value // Butcher
jobIter.next().value // Baker
jobIter.next().value // Candlestick maker
let p = new Person()
let nicknameIter = p.createNicknameIterator()
nicknameIter.next().value // Jack
nicknameIter.next().value // Jake
nicknameIter.next().value // J-Dog
- 因为支持生成器方法,可以通过添加一个默认迭代器,把类实例变为可迭代对象
class Person {
constructor() {
this.nickname = ['Jack', 'Jake', 'J-Dog']
}
*[Symbol.iteator]() {
yield *this.nickname.entries()
}
}
let p = new Person()
for(let [idx, nickname] of p) {
console.log(nickname)
}
- 也可以只返回迭代器实例
class Person {
constructor() {
this.nickname = ['Jack', 'Jake', 'J-Dog']
}
[Symbol.iteator]() {
return this.nickname.entries()
}
}
let p = new Person()
for(let [idx, nickname] of p) {
console.log(nickname)
}
4、继承
- 继承虽然是新语法,但背后依旧使用的是原型链
1、继承基础
- ES6支持单继承,使用extends关键字,就可以继承任何拥有
[[Construct]]
和原型的对象 - 不仅可以继承类也可以继承普通构造函数
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus()
b instanceof Bus // true
b instanceof Vehicle // true
function Person()
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer()
e instanceof Engineer // true
e instanceof Person // true
-
派生类都会通过原型链访问到类和原型上定义的方法。
- this的值会反映调用相应方法的实例或者类
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 {}
-
extends 也可以在类表达式使用,如
let Bar = class extends Foo{}
2、构造函数、HomeObject和super()
-
派生类的方法可以通过super来引用它们的原型
-
只能在派生类中使用,而且仅限类构造函数、实例方法和静态方法内部
-
类构造函数中super可以调用父类构造函数
class Vehicle { constructor() { this.hasEngine = true } } class Bus extends Vehicle { constructor() { // 不要在super之前用this super() //等同于super.constructor() console.log(this instanceof Vehicle) // true console.log(this) // Bus {hasEngine: true} } } new Bus()
-
在静态方法中可以通过super调用继承的类上定义的静态方法
class Vehicle { static identify() { console.log('vehicle') } } class Bus extends Vehicle { static identify() { super.identify() } } Bus.identify() // vehicle
-
[[HomeObject]]
- ES6给类构造函数和静态方法添加的内部特征
- 是一个指针,指向定义改方法的对象
- 这个指针是自动赋值的,只能在JavaScript引擎内部访问
- super始终会定义为[[HomeObject]]的原型
-
使用super需要注意
-
super只能在派生类构造函数和静态方法中使用
class Bus { constructor() { super() //报错 } }
-
不能单独引用super,要么调用构造函数,要么用它引用静态方法
-
调用super() 会调用父类构造函数,并将返回的实例复制给this
class Vehicle {} class Bus extends Vehicle { constructor() { super() console.log(this instanceof Vehicle) } } new Bus() // true
-
super() 的行为如同调用构造函数,如需给父类构造函数传参需要手动传入。
class Vehicle { constructor(licensePlate) { this.licensePlate = licensePlate } } class Bus extends Vehicle { constructor(licensePlate) { super(licensePlate) } } new Bus('1222') // Bus {licensePlate: '1222'}
-
如果没有定义类构造函数,在实例化派生类时会调用super() ,而且会传入所有传给派生类的参数。
class Vehicle { constructor(licensePlate) { this.licensePlate = licensePlate } } class Bus extends Vehicle {} new Bus('1122') // Bus {licensePlate: '1122'}
-
在类构造函数中,不能在调用super() 之前引用this
-
如果在派生类中显式定义了构造函数,则要么必须要在其中调用super(),要么必须在其中返回一个对象
class Vehicle {} class Car extends Vehicle {} class Bus extends Vehicle { constructor() { super() } } class Van extends Vehicle { constructor() { return {} } } new Car() // Car {} new Bus() // Bus {} new Van() // {}
-
3、抽象基类
-
可以供其他类继承,但本身不会被实例化。
-
通过new.target 可以实现,new.target保存通过new关键字调用的类或函数。通过在实例化时检测new.target是不是抽象基类,阻止对抽象基类的实例化
class Vehicle { conscructor() { console.log(new.target) if(new.target === Vehicle) { throw new Error('....') } } } class Bus extends Vehicle {} new Bus() // class Bus {} new Vehicle() // class Vehicle {} // Error: ....
-
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就存在了,所以可以通过this关键字检查相应方法
class Vehicle { constructor() { if(new.target === Vehicle) { throw new Error('.....') } if(!this.foo) { throw new Error('Inheriting class ...') } console.log('success!') } } class Bus extends Vehicle { foo() {} } class Van extends Vehicle {} new Bus() // success new Van() // Error: Inheriting ...
4、继承内置类型
-
ES6类为继承内置引用类型提供了顺畅的机制,可以来扩展内置类型
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) a instanceof Array // true a instanceof SuperArray // true a // [1,2,3,4,5] a.shuffle() a // [3,1,4,2,5]
-
有些内置方法会返回新实例。默认返回实例的类型和原始实例类型一致
class SuperArray extends Array {} let a1 = new SuperArray(1,2,3,4,5) let a2 = a1.filter(x=>!!(x%2)) a1 // [1,2,3,4,5] a2 // [1,3,5] a1 instanceof SuperArray // true a2 instanceof SuperArray // true
-
如果想覆盖这个默认行为,可以覆盖Symbol.species 访问器,这个访问器决定在创建返回的实例时返回的类
class SuperArray extends Array { static get [Symbol.species]() { return Array } } let a1 = new SuperArray(1,2,3,4,5) let a2 = a1.filter(x=>!!(x%2)) a1 // [1,2,3,4,5] a2 // [1,3,5] a1 instanceof SuperArray // true a2 instanceof SuperArray // false
5、类混入
-
把不同类的行为集中到一个类,没有显式支持多类继承,但可以通过现有特征模拟
-
Object.assign()方法是为了混入对象行为而设计的。在需要混入类的行为是才有必要自己实现混入表达式,如果只是混入多个对象的属性使用Object.assign()即可。
class Vehicle {} function getParentClass() { console.log('xxx') return Vehicle } class Bus extends getParentClass() {} // 可求值的表达式
-
混入模式可以通过在一个表达式中连缀多个混入元素实现
-
最终会被解析为一个可以被继承的类。
-
如果Person类需要组合A、B、C,则需要某种机制实现B继承A,C继承B,Person继承C
-
策略:定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀使用
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))) 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() b.bar() b.baz()
-
目前大多数框架都放弃混入模式,转向组合模式(把方法提取到独立的类和辅助对象里,然后把它们组合起来,但不使用继承),反映了"组合胜过继承"的原则,更灵活。