面向对象编程
对象基础
ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把ECMAScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。
let person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name)
},
}
属性的类型
数据属性
configurable
表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特
性都是true
Enumerable
表示属性是否可以通过for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true
Writable
表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true
Value
包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为undefined。
要修改属性的默认特性,就必须使用Object.defineProperty()方法
defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty(obj, prop, descriptor)
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或 Symbol 。
descriptor
要定义或修改的属性描述符。
let person = {}
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas",
})
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
访问器属性
Configurable
表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
Enumerable
表示属性是否可以通过for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
Get
获取函数,在读取属性时调用。默认值为undefined
Set
设置函数,在写入属性时调用。默认值为undefined
访问器属性是不能直接定义的,必须使用Object.defineProperty()
// 定义一个对象,包含伪私有成员year_和公共成员edition
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
定义多个属性
defineProperties()
方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象
Object.defineProperties(obj, props)
obj
在其上定义或修改属性的对象。
props
要定义其可枚举属性或修改的属性描述符的对象。对象中存在的属性描述符主要有两种:数据描述符和访问器描述符
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 } }, },})
读取属性的特性
getOwnPropertyDescriptor()
方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
Object.getOwnPropertyDescriptor(obj, prop)
obj
需要查找的目标对象
prop
目标对象内属性名称
let book = {}
Object.defineProperties(book, {
year_: {
value: 2017,
},
edition: {
value: 1,
},
year: {
get: function () {
return this.year_
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue
this.edition += newValue - 2017
}
},
},
})
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"
getOwnPropertyDescriptors()
方法用来获取一个对象的所有自身属性的描述符。
Object.getOwnPropertyDescriptors(obj)
obj
任意对象
let book = {}
Object.defineProperties(book, {
year_: {
value: 2017,
},
edition: {
value: 1,
},
year: {
get: function () {
return this.year_
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue
this.edition += newValue - 2017
}
},
},
})
console.log(Object.getOwnPropertyDescriptors(book))
// {
// 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()
ECMAScript6
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
Object.assign(target, ...sources)target目标对象。sources源对象
const obj = { a: 1 };const copy = Object.assign({}, obj);console.log(copy); // { a: 1 }
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值
不能在两个对象间转移获取函数和设置函数
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: src2, a: foo, b: bar }
浅复制意味着只会复制对象的引用
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
对象标识及相等判定
ECMAScript6
Object.is()
Object.is()
方法判断两个值是否为同一个值。
Object.is()
方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:
与==
运算不同。 ==
运算符在判断相等前对两边的变量(如果它们不是同一类型) 进行强制转换 (这种行为的结果会将 "" == false
判断为 true
), 而 Object.is
不会强制转换两边的值。
与===
运算也不相同。 ===
运算符 (也包括 ==
运算符) 将数字 -0
和 +0
视为相等 ,而将Number.NaN
与NaN
视为不相等.
Object.is(value1, value2);
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
增强的对象语法
属性值简写
let name = 'Matt';
let person = {
name: name
};
console.log(person); // { name: 'Matt' }
let name = 'Matt';
let person = {
name
};
console.log(person); // { name: 'Matt' }
可计算属性
如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。
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' }
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 person = {
sayName: function (name) {
console.log(`My name is ${name}`)
},
}
person.sayName("Matt") // My name is Matt
let person = { sayName(name) { console.log(`My name is ${name}`) },}person.sayName("Matt") // My name is Matt
const methodKey = "sayName"let person = { [methodKey](name) { console.log(`My name is ${name}`) },}person.sayName("Matt") // My name is Matt
对象解构
ECMAScript 6 新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。
// 不使用对象解构let person = {name: 'Matt',age: 27};let personName = person.name, personAge = person.age;console.log(personName); // Mattconsole.log(personAge); // 27
// 使用对象解构
let person = {
name: "Matt",
age: 27,
}
let { name: personName, age: personAge } = person
console.log(personName) // Matt
console.log(personAge) // 27
let person = {
name: "Matt",
age: 27,
}
let { name, age } = person
console.log(name) // Matt
console.log(age) // 27
嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制
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' } }
解构赋值可以使用嵌套结构,以匹配嵌套的属性
let person = {
name: "Matt",
age: 27,
job: {
title: "Software engineer",
},
}
// 声明title 变量并将person.job.title 的值赋给它
let {
job: { title },
} = person
console.log(title) // Software engineer
部分解构
涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分
let person = { name: "Matt", age: 27,}let personName, personBar, personAgetry { // person.foo 是undefined,因此会抛出错误 ;({ name: personName, foo: { bar: personBar }, age: personAge, } = person)} catch (e) {}console.log(personName, personBar, personAge)// Matt, undefined, undefined
参数上下文匹配
在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响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
创建对象
工厂模式
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("Nicholas", 29, "Software Engineer")
let person2 = createPerson("Greg", 27, "Doctor")
构造函数模式
构造函数首字母必须大写
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")
person1.sayName() // Nicholas
person2.sayName() // Greg
使用new 操作符,会执行如下操作
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype 属性。
(3) 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
//批量制作
function study(ID){
var ability = {}
ability._proto_ = people._proto_
ability.ID =ID
return ability
}
var peopel._proto_ = {
species:'人'
say:function(){},
foot:function(){}
}
var studys = []
for(var i =0;i<100;i++) {
studys.push(study(i))
}
//使用new后
function Study(ID) {
this.ID = ID
}
Study.prototype = {
species:'人'
say:function(){},
foot:function(){}
}
var studys = []
for(var i=0;i<100;i++) {
studys.push(new study(i))
}
使使用new
不用创建临时对象
不用绑定原型
不用return临时对象
构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。任何函数只要使用new 操作符调用就是构造函数,而不使用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"
构造函数的问题
定义的方法会在每个实例上都创建一遍
原型模式
每个函数都会创建一个prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法
这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型
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();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
理解原型
原型实际上就是一个普通对象,继承于 Object 类,由 JavaScript 自动创建并依附于每个函数身上
默认情况下,所有原型对象自动获得一个名为constructor 的属性,指回与之关联的构造函数
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
Object.isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上
prototypeObj.isPrototypeOf(object)
function Foo() {}
function Bar() {}
function Baz() {}
Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);
var baz = new Baz();
console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true
Object.getPrototypeof()
方法返回指定对象的原型(内部[[Prototype]]
属性的值)
Object.getPrototypeOf(object)
const prototype1 = {};
const object1 = Object.create(prototype1);
console.log(Object.getPrototypeOf(object1) === prototype1);
// expected output: true
Object.setPrototypeOf()
方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null
。
Object.setPrototypeOf(obj, prototype)
obj
要设置其原型的对象。
prototype
该对象的新原型(一个对象 或 null)
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
原型层级
var obj = {}obj.toString()
调用 obj.toString() 的时候,obj 本身没有 toString,就去 obj.proro 上面去找 toString。
调用 obj.toString 的时候,实际上调用的是 window.Object.prototype.toString
obj.toString() 等价于 obj.toString.call(obj)
同时 obj.toString.call(obj) 等价于 window.Object.prototype.toString.call(obj)
prototype 指向一块内存,这个内存里面有共用属性
proto 指向同一块内存
prototype 和 *proto* 的不同点在于
prototype 是构造函数的属性,而 proto 是对象的属性
对象迭代
ECMAScript 2017 新增了两个静态方法
Object.values()
方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in
循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。
Object.values(obj)
var obj = { foo: 'bar', baz: 42 };
console.log(Object.values(obj)); // ['bar', 42]
// array like object
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.values(obj)); // ['a', 'b', 'c']
// array like object with random key ordering
// when we use numeric keys, the value returned in a numerical order according to the keys
var an_obj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.values(an_obj)); // ['b', 'c', 'a']
// getFoo is property which isn't enumerable
var my_obj = Object.create({}, { getFoo: { value: function() { return this.foo; } } });
my_obj.foo = 'bar';
console.log(Object.values(my_obj)); // ['bar']
// non-object argument will be coerced to an object
console.log(Object.values('foo')); // ['f', 'o', 'o']
Object.entries()
方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in
循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。
Object.entries(obj)
const obj = { foo: 'bar', baz: 42 };
console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]
// array like object
const obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.entries(obj)); // [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ]
// array like object with random key ordering
const anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.entries(anObj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]
// getFoo is property which isn't enumerable
const myObj = Object.create({}, { getFoo: { value() { return this.foo; } } });
myObj.foo = 'bar';
console.log(Object.entries(myObj)); // [ ['foo', 'bar'] ]
// non-object argument will be coerced to an object
console.log(Object.entries('foo')); // [ ['0', 'f'], ['1', 'o'], ['2', 'o'] ]
// iterate through key-value gracefully
const obj = { a: 5, b: 7, c: 9 };
for (const [key, value] of Object.entries(obj)) {
console.log(`${key} ${value}`); // "a 5", "b 7", "c 9"
}
// Or, using array extras
Object.entries(obj).forEach(([key, value]) => {
console.log(`${key} ${value}`); // "a 5", "b 7", "c 9"
});
理解对象
前言
function Foo(){....}
let f1 = new Foo();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZtIZjEda-1621323423167)(images/%20javascript/20190311194017886.png)]
__proto__属性
①__proto__
和constructor
属性是对象所独有的;
② prototype
属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__
和constructor
属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4cDPY6sE-1621323423171)(images/%20javascript/20190311192930650.png)]
__proto__属性
对象独有
__proto__
属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象)它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null
prototype属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s942jMHf-1621323423173)(images/%20javascript/20190311193033876.png)]
prototype属性
函数独有从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象
作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。
constructor属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBb6mb4o-1621323423177)(images/%20javascript/20190311193745414.png)]
constructor属性
是对象独有的是指向该对象的构造函数,每个对象都有构造函数(本身拥有或继承而来)
所有函数和对象最终都是由Function构造函数得来,所以
constructor
属性的终点就是Function这个函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iYSthPyL-1621323423180)(images/%20javascript/20190311193622793.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r4pCCFdB-1621323423181)(images/%20javascript/20190311192013184.png)]
继承
实现继承是ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
原型链
var obj = {name:'obj'}
obj.valueOf()
当我们「读取」 obj.toString 时,JS 引擎会做下面的事情:
1.看看 obj 对象本身有没有 toString 属性。没有就走到下一步。
2.看看 obj.proto 对象有没有 toString 属性,发现 obj.proto 有 toString 属性,于是找到了
所以 obj.toString 实际上就是第 2 步中找到的 obj.proto.toString。
可以想象,
3.如果 obj.proto 没有,那么浏览器会继续查看 obj.proto.proto
4.如果 obj.proto.proto 也没有,那么浏览器会继续查看 obj.proto.proto.proto__
5.直到找到 toString 或者 proto 为 null。
上面的过程,就是「读」属性的「搜索过程」。
而这个「搜索过程」,是连着由 proto 组成的链子一直走的。
这个链子,就叫做「原型链」。
默认原型
默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的
任何函数的默认原型都是一个Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。
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
SubType 继承SuperType,而SuperType 继承Object
原型与继承关系
原型与实例的关系可以通过两种方式来确定
instanceof
如果一个实例的原型链中出现过相应的构造函数,则instanceof 返回true
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
isPrototypeOf()
原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,这个方法就返回true:
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
关于方法
子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上
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
以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链
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() {
return this.subproperty
},
someOtherMethod() {
return false
},
}
let instance = new SubType()
console.log(instance.getSuperValue()) // 出错!
原型链的问题
原型中包含的引用值会在所有实例间共享,在使用原型实现继承时,原型实际上变成了另一个类型
的实例
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"
子类型在实例化时不能给父类型的构造函数传参,无法在不影响所有对象实例的情况下把参数传进父类的构造函数
盗用构造函数
为了解决原型包含引用值导致的继承问题
**思路:**在子类构造函数中调用父类构造函数
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"
传递参数
优点就是可以在子类构造函数中向父类构造函数传参
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
盗用构造函数的问题
必须在构造函数中定义方法,因此函数不能重用,子类也不能访问父类原型上定义的方法
组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来
**思路:**使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
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
原型式继承
2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance in
JavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例
本质上,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"
ECMAScript 5 通过增加Object.create()方法将原型式继承的概念规范化
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
Object.create(proto,[propertiesObject])
proto
新创建对象的原型对象。
propertiesObject
可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符
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"
寄生式继承
与原型式继承比较接近的一种继承方式是寄生式继承(parasitic inheritance),也是Crockford 首倡的一种模式
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"
寄生式组合继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用
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)
}
// 寄生组合继承:这个过程既实现了继承,又没有去调用Super
function inheritPrototype(Sub,Super){
//subPrototype.__proto__=Super.prototype
var subPrototype=Object.create(Super.prototype)
//subPrototype.constructor=Sub
subPrototype.constructor=Sub
//相当于subPrototype有__proto__和constructor两个属性
//即:
//Sub.prototype.__proto__===Super.prototype
//Sub.prototype.constructor=Sub
Sub.prototype=subPrototype
}
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)
}
类
ECMAScript 6 新引入的class 关键字具有正式定义类的能力
类定义
// 类声明
class Person {}
// 类表达式
const Animal = class {};
类构成
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法
// 空类定义,有效
class Foo {}
// 有构造函数的类,有效
class Bar {
constructor() {}
}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}
类表达式的名称是可选的,但不能在类表达式作用域外部访问这个标识符。
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 关键字用于在类定义块内部创建类的构造函数
方法名constructor 会告诉解释器在使用new 操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。
实例化
使用new 操作符实例化Person 的操作等于使用new 调用其构造函数
使用new 调用类的构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype 属性。
(3) 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
class Animal {}
class Person {
constructor() {
console.log("person ctor")
}
}
class Vegetable {
constructor() {
this.color = "orange"
}
}
let a = new Animal()
let p = new Person() // person ctor
let v = new Vegetable()
console.log(v.color) // orange
类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:
class Person {
constructor(name) {
console.log(arguments.length)
this.name = name || null
}
}
let p1 = new Person() // 0
console.log(p1.name) // null
let p2 = new Person() // 0
console.log(p2.name) // null
let p3 = new Person("Jake") // 1
console.log(p3.name) // Jake
类构造函数实例化之后,会成为普通的实例方法
class Person {}
// 使用类创建一个新实例
let p1 = new Person();
p1.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();
把类当成特殊函数
ECMAScript 类就是一种特殊函数。声明一个类之后,通过typeof 操作符检测类标识符,表明它是一个函数:
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function
类标识符有prototype 属性,而这个原型也有一个constructor 属性指向类自身:
class Person{}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true
实例、原型和类成员
实例成员
每次通过new 调用类标识符时,都会执行类构造函数。在这个函数内部,可以为新创建的实例(this)添加“自有”属性
class Person {
constructor() {
// 这个例子先使用对象包装类型定义一个字符串
// 为的是在下面测试两个对象的相等性
this.name = new String("Jack")
this.sayName = () => console.log(this.name)
this.nicknames = ["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.nicknames === p2.nicknames) // false
p1.name = p1.nicknames[0]
p2.name = p2.nicknames[1]
p1.sayName() // Jake
p2.sayName() // J-Dog
原型方法与访问器
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
class Person {
constructor() {
// 添加到this 的所有内容都会存在于不同的实例上
this.locate = () => console.log("instance")
} // 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log("prototype")
}
}
let p = new Person()
p.locate() // instance
Person.prototype.locate() // prototype
可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据:
class Person {
name: 'Jake'
}
// Uncaught SyntaxError: Unexpected token
类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键:
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
类定义也支持获取和设置访问器
class Person {
set name(newName) {
this.name_ = newName
}
get name() {
return this.name_
}
}
let p = new Person()
p.name = "Jake"
console.log(p.name) // Jake
静态类方法
可以在类上定义静态方法
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 {}
静态类方法非常适合作为实例工厂
class Person {
constructor(age) {
this.age_ = age
}
sayAge() {
console.log(this.age_)
}
static create() {
// 使用随机年龄创建并返回一个Person 实例
return new Person(Math.floor(Math.random() * 100))
}
}
console.log(Person.create()) // Person { age_: ... }
非函数原型和类成员
类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加:
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
迭代器与生成器方法
类定义语法支持在原型和类本身上定义生成器方法:
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
可以通过添加一个默认的迭代器,把类实例变成可迭代对象:
class Person {
constructor() {
this.nicknames = ["Jack", "Jake", "J-Dog"]
}
*[Symbol.iterator]() {
yield* this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// Jack
// Jake
// J-Dog
class Person {
constructor() {
this.nicknames = ["Jack", "Jake", "J-Dog"]
}
[Symbol.iterator]() {
return this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// Jack
// Jake
// J-Dog
继承
继承基础
ES6 类支持单继承。使用extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。
意味着不仅可以继承一个类,也可以继承普通的构造函数
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 的值会反映调用相应方法的实例或者类:
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 可以调用父类构造函数。
class Vehicle {
constructor() {
this.hasEngine = true
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用super()之前引用this,否则会抛出ReferenceError
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
ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在JavaScript 引擎内部访问。super 始终会定义为[[HomeObject]]的原型
super 只能在派生类构造函数和静态方法中使用。
class Vehicle {
constructor() {
super()
// SyntaxError: 'super' keyword unexpected
}
}
不能单独引用super 关键字,要么用它调用构造函数,要么用它引用静态方法。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super)
// SyntaxError: 'super' keyword unexpected here
}
}
调用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)
}
}
console.log(new Bus("1337H4X")) // Bus { licensePlate: '1337H4X' }
如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数。
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {}
console.log(new Bus("1337H4X")) // Bus { licensePlate: '1337H4X' }
在类构造函数中,不能在调用super()之前引用this。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this)
}
}
new Bus()
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象。
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
}
}
class Van extends Vehicle {
constructor() {
return {}
}
}
console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}
抽象基类
供其他类继承,但本身不会被实例化。
**原理:**通过在实例化时检测new.target 是不是抽象基类,阻止对抽象基类的实例化
// 抽象基类
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 cannot be directly instantiated
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法
// 抽象基类
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()
继承内置类型
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); // [3, 1, 4, 5, 2]
默认情况下,返回实例的类型与原始实例的类型是一致的:
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))
console.log(a1) // [1, 2, 3, 4, 5]
console.log(a2) // [1, 3, 5]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // false
覆盖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))
console.log(a1) // [1, 2, 3, 4, 5]
console.log(a2) // [1, 3, 5]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // false
类混入
Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。
extends 关键字后面是一个JavaScript 表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。
class Vehicle {}
function getParentClass() {
console.log("evaluated expression")
return Vehicle
}
class Bus extends getParentClass() {}
// 可求值的表达式
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
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
很多JavaScript 框架(特别是React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。