对象、原型、继承、类
参考javascript红宝书第8章。
对象:
对象包含属性和方法,通常用对象字面量来定义:
let person = {
name:'Mike',
age:18,
sayName(){
console.log(this.name)
}
}
创建了名为person的对象,有两个属性(name和age)和一个方法(sayName())。
属性的特征:
有两种类型的属性:数据属性和访问器属性。两者的特性都得经过Object.defineProperty()来修改和定义。
- 数据属性:包含保存数据值的位置
Configurable:意味能否将该属性delete删除;
Enumerable:能否通过for...in...遍历到;
Writable:属性值能够被修改;
value:属性实际的值
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Mike",
});
- 访问器属性:不包含数据值,包含获取(setter)和(getter)函数。进行设置和读取
Configurable:意味能否将该属性delete删除;
Enumerable:能否通过for...in...遍历到;
Get:获取函数,读取属性时调用
Set:设置函数,在写入属性时使用
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
book有两个属性year_和edition。year_下划线常用来表示该属性不希望被对象的外部访问到。另一个属性year被定义为访问器属性。
合并对象:
Object.assign(targetObject,originalObject);将一个或者多个对象的所有可枚举属性复制到目标对象上。实际上是浅复制。
访问的属性不在对象中,那么就会进一步访问对象的原型链。
语法糖:
- 属性值简写:
let name = "Mike";
let person = {name:name};
//属性名和值相等时,可以简写一个,即等价于
let person = {name}
- 如果想要使用变量作为属性,不能直接用字面量来动态命名属性,得用[ ]。
const nameKey = 'name';
const jobKey = 'job';
let person = {
nameKey: 'Mike',
jobKey:'engineer'
}
console.log(person)//{nameKey: 'Mike', jobKey: 'engineer'}
let person2 = {
[nameKey]: "Mike",
[jobKey]: "engineer",
};
console.log(person2);//{name: 'Mike', job: 'engineer'}
- 简写方法名
对象定义方法时,通常是一个方法名,冒号,然后一个匿名函数表达式。可简写为:
let person = {
sayName:function(name){
console.log(name);
}
}
//上述标准形式可简写为:
let person = {
sayName(name){
console.log(name);
}
}
- 对象解构
let person = {
name: 'Mike',
age: 18
hobbit:{
title:'study'
}
}
//不采用解构
let personName = person.name;//Mike
let personAge = person.age;//18
//解构,将属性名为name和age的值分别赋值到personName和personAge当中
let {name:personName,age:personAge} = person
//方式二,变量直接使用属性名称,更简写
let {name,age} = person;
console.log(name)//'Mike'
console.log(age)//18
//解构赋值的属性如果不存在,该变量值就是undefined
let {name,job} = person;
console.log(job)//undefined
//解构可以赋值初始值
let {name,job = 'engineer'} = person;
console.log(job)//engineer
//嵌套解构,引用值会共享
let personCopy = {};
({name:personCopy.name,
age:personCopy.age,
hobbit:personCopy.hobbit
}= person);
//嵌套解构赋值
let {hobbit:{title}} = person;
console.log(title);//study
//arguments进行结构赋值
function printPerson({name,age}){
console.log(name,age)
}
printPerson(person)//Mike 18
解构赋值是一个顺序化操作。如果只有部分解构,中间出现问题的时候,就会解构中断,后续的结构都不会成功取得值。
构造函数模式
构造函数创建方式和普通函数一样,不同是构造函数习惯首字母大写,需要用new关键字来调用;普通函数直接调用。
构造函数模式可以使用new来创建实例
function Person(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
this.sayName = function () {
alert(this.name);
}
}
var per = new Person("孙悟空", 18, "男");
console.log(per);//Person {name: '孙悟空', age: 18, gender: '男', sayName: ƒ}
per.sayName();//孙悟空
缺点:每次new实例,都会有新的sayname函数,但其实是一样的操作,没必要多次创建。
解决问题方法就是原型模式。
new操作符调用构造函数,具体包含以下步骤:
- 创建一个新对象;
- 将对象的prototype属性被赋值为构造函数的prototype属性;
- 将构造函数内部的this指向这个新对象
- 执行构造函数内部的代码(给对象增添属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
原型模式
构造函数都有一个prototype属性,这个属性是一个对象,叫做原型对象。其包含所有实例共享的属性和方法。
实例和实例原型有直接关系,而与构造函数之间没有。
function Person() {}
let friend1 = new Person();
let friend2 = new Person();
Person.prototype = {
constructor: Person, //字面量定义prototype覆盖了本身的原型,无法指向constructor,手动指向
name: "Rose",
age: 18,
friends: ["Jack", "Cindy"],
sayName() {
console.log(this.name);
},
};
friend.sayName();//无法调用,因为指向的还是原生的prototype,没有sayname属性。
friend1和friend2共享属性和方法,但是这种形式实例无法直接引用方法。
单独定义的时候Person.prototype.name = "Rose"可以共享并且引用,但是写起来很麻烦。
缺点:主要是引用值的共享。上述friend1想新增一个朋友Andy,但是friend2也会新增上此朋友。
\
- 原型与for...in...
var myObj = {
a: 2,
};
console.log("a" in myObj);
console.log("b" in myObj);
console.log(myObj.hasOwnProperty("a"));
console.log(myObj.hasOwnProperty("b"));
in会检查属性是否在对象及其原型链当中;in检查的是属性名。例如数组当中[2,4,5],4 in [2,4,5]会报false,因为数组的属性名是0,1,2,没有4.
而hasOwnProperty只会检查属性是否在对象当中,不会检查原型链。
enumerable枚举属性禁止之后,in和hasOwnProperty都能够确认存在该属性,但for...in...无法便利到该属性。
\
Object.keys()可以返回一个数组,包含所有可枚举属性;object.getOwnPropertyNames()返回一个数组,包含所有属性,无论是否可枚举。这两者都只会查找对象直接包含的属性,不会查找原型链。
\
继承
原型链
如果原型是另外一个类型的实例,表明这个原型本身有一个内部指针指向另一个原型,另一个原型也有一个指针指向另一个构造函数。这就是原型链。
function Father() {
this.hasMoney = true;
}
Father.prototype.getFatherVale = function () {
return this.hasMoney;
};
function Son() {
this.hasToy = true;
}
//父类的实例赋值给子类的原型
Son.prototype = new Father();
Son.prototype.getSonVale = function () {
return this.hasToy;
};
let instance = new Son();
console.log(instance.getFatherVale());//true
所有引用类型都继承自Object
当子类型需要定义新的方法或者覆盖父类的方法时。在原型赋值之后再添加到原型上。
function Father() {
this.hasMoney = true;
this.colors = ["red", "blue", "pink"];
}
Father.prototype.getFatherVale = function () {
return this.hasMoney;
};
function Son() {
this.hasToy = true;
}
//父类的实例赋值给子类的原型
Son.prototype = new Father();
//新方法
Son.prototype.getSonVale = function () {
return this.hasToy;
};
//覆盖父类方法
Son.prototype.getFatherVale = function () {
return false;
};
let instance = new Son();
console.log(instance.getFatherVale());//false
//引用值共享问题
let instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//['red', 'blue', 'pink', 'black']
let instance2 = new Son();
console.log(instance2.colors);//['red', 'blue', 'pink', 'black']
问题:
- 原型中的引用值会在所有实例之间共享
- 子类型实例化时不能给父类传参
盗用构造函数(经典继承)
如果子类在构造函数中调用父类构造函数,那么就会运行父构造函数中的所有初始化代码,每个实例都会有自己的属性。
function Father(name) {
this.colors = ["red", "blue", "pink"];
this.name = name;
}
function Son() {
//继承,并传参
Father.call(this,'Mike');
}
//实例都有各自的引用值
let instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//['red', 'blue', 'pink', 'black']
let instance2 = new Son();
console.log(instance2.colors);//['red', 'blue', 'pink']
//能传参
console.log(instance1.name);
问题:
必须在构造函数中定义方法,因此函数不能重用。子类也无法访问父类原型上定义的方法。(见构造函数模式那一节),call继承之后,将所有的方法、引用值都重新创建。但方法都是一样的,没必要多次创建,子类创建完之后相当于给父类复制了一遍,无法访问父类原型上定义的方法。
组合继承
通过构造函数可以定义实例属性,通过原型可以定义共享的属性与方法,那么两者结合起来就是组合继承。
function Father(name) {
this.colors = ["red", "blue", "pink"];
this.name = name;
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son(name, age) {
//继承属性
Father.call(this, name);//第一次调用父构造函数
this.age = age;
}
//继承方法
Son.prototype = new Father();//第一次调用父构造函数
//由于Son原型进行单独赋值了,因此constructor不再指向Son了,需要重新指向一下。
//直接打印Son.prototype.constructor现实的是Father函数
//此部分重新指向constructor在红宝书第四版中没有提及,但之前的版本有,所以注释起来了
//Son.prototype.constructot = Son
Son.prototype.sayAge = function () {
console.log(this.age);
};
let instance1 = new Son("Mike", 18);
instance1.colors.push("black");
console.log(instance1.colors); //['red', 'blue', 'pink', 'black']
instance1.sayAge();//18
instance1.sayName();//Mike
let instance2 = new Son("Jack", 20);
console.log(instance2.colors); //['red', 'blue', 'pink']
instance2.sayAge();//20
instance2.sayName();//Jack
问题:
父类构造函数会被调用两次,一次在创建子类原型时候调用,另外一次在子构造函数中调用。有效率问题。
原型式继承
Object.create(),适用于不创建构造函数的情况下,仍需要在对象间共享信息的场合,此时属性的引用值在所有对象中是共享的。其原理如下,相当于浅复制。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
适用场景:你有一个对象,需要基于它创建一个新对象,然后进行适当修改。
function Father(name) {
this.colors = ["red", "blue", "pink"];
this.name = name;
}
let person = { name: "Mike", friends: ["Jack", "Cindy"] };
let person1 = Object.create(person);
person1.name = "Rose";
person1.friends.push("Linda");
console.log(person.friends);//['Jack', 'Cindy', 'Linda']
let person2 = Object.create(person);
person2.name = "Greg";
person2.friends.push("Van");
console.log(person.friends);// ['Jack', 'Cindy', 'Linda', 'Van']
console.log(person2.friends);// ['Jack', 'Cindy', 'Linda', 'Van']
寄生式继承
创建一个实现继承的函数。增强对象,返回对象。用于主要关注对象,不在乎类型和构造函数的场景。
主要思路:首先,取得原先对象的副本;然后单独定义额外的方法。
function creatAnother(original){
let clone = Object.create(original);
clone.sayHi = function(){
console.log('hi');
return clone;
}
}
类似于原型式继承调用 Object.create(),寄生式继承调用这个继承函数creatAnother(),在原对象的基础上,增添了sayHi方法。
问题:添加的函数会导致难以重复使用,类似构造函数模式。
寄生组合继承
回顾一下组合继承的两次调用父构造函数问题。
function Father(name) {
this.colors = ["red", "blue", "pink"];
this.name = name;
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son(name, age) {
//继承属性
Father.call(this, name);//第一次调用父构造函数
this.age = age;
}
//继承方法
Son.prototype = new Father();//第一次调用父构造函数
let instance = new Son("Mike", 18);
两次调用构造函数时,第一次,Son.prototype中包含了colors和name属性。第二次let instance = new Son("Mike", 18);第二次调用,使得实例当中也包含了colors和name属性。因此最终有两组属性,一组在实例,一组在子类原型上。
采用寄生组合式继承,即可解决上述问题。
function Father(name) {
this.colors = ["red", "blue", "pink"];
this.name = name;
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son(name, age) {
//继承属性
Father.call(this, name);
this.age = age;
}
//继承方法
let prototype = Object.create(Father.prototype);
prototype.constructor = Son;//解决重写原型导致constructor丢失。
Son.prototype = prototype;
let instance = new Son("Mike", 18);
不是通过父构造函数给子原型赋值,而是取得父原型的副本,使用寄生式继承来继承父原型。
父原型的继承采取原型式继承 Object.create(Father.prototype),可使得父原型对象与新原型对象之间共享信息;然后将新原型的构造函数指向子构造函数。最后将返回的新原型赋值给子类原型。
优点:解决了两次调用父构造函数的问题,也避免了Son.prototype上不必要也用不到的属性,效率更高。此为引用类型继承的最佳模式。
类
类定义有两种:类声明和类表达式。
class Person{};
const Animal = class{};
与函数区别:
- 函数声明可以提升,但类定义不行。
- 函数受函数作用域限制,类受块作用域限制。
类可包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。
类构造函数
new实例化类的时候,等于使用new调用其构造函数。
class Person {
constructor() {
console.log("nice");
}
}
let p = new Person();//nice
- 当构造函数有返回值,且返回的其他对象而不是this对象时,其new创建的对象不会通过instanceof操作符检测出和类有关联,因为对象的原型指针没有被修改。
- 调用类构造函数必须new,没有会报错。而普通构造函数不new时候,会以全局的this作为内部对象。
class Person {
constructor() {
console.log("nice");
}
}
//用类类创建实例
let p = new Person();
//实例直接调用constructor会报错
p.constructor()//typeError
//调用类构造函数必须使用new
//用类构造函数创建实例
let p1 = new p.constructor()
- 类标识符有prototype属性,这个原型也有constructor属性指向类自身。与普通构造函数不同的是,instanceof可确定实例和普通构造函数之间的关系;而类中,instanceof确定的是实例与类的关系,而不是类构造函数。
class Person{};
Person.prototype.constructor = Person;//true
let p = new Person();
p.constructor ===Person//true
p instanceof Person//true
p instanceof Person.constructor//false
实例、原型和类成员
- 类的每一个实例都对应一个成员对象,所有成员不会在原型上共享。都是一个独立的个体,不会信息共享。
- 那么,怎么实现实例间共享方法呢,就是在类块中定义方法,那么此方法就会成为类原型上的方法。日常一般方法都是用来实例共用的,会定义在类块中。
- 此外,不能在类块中给原型添加原始值或对象作为成员数据,会报错Unexpected token
class Person {
constructor() {
this.locate = () => {
console.log("instance");
};
}
//常见的方法定义
locate() {
console.log("prototype");
}
name:'jake'//报错,Unexpected token,
}
let p = new Person();
p.locate();//instance
Person.prototype.locate();//prototype
类也支持设置和获取访问器。
- 静态类方法,这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个。
class Person {
constructor() {
//添加到this的所有内容都存在于不同的实例上
this.locate = () => {
console.log("instance");
};
}
//定义在类原型上
locate() {
console.log("prototype");
}
//*****************
//静态类方法
//*****************
//定义在类本身上
static locate() {
console.log("class");
}
}
let p = new Person();
p.locate();//instance
Person.prototype.locate();//prototype
Person.locate();//class
- 在类原型和类本身上可定义生成器方法
继承
类可以继承类,也可以继承普通构造函数。用extends关键字.
派生类可以通过原型链访问到类和原型上定义的方法,this的值会反映调用的相应方法的实例或者类。
class Vehicle{
identify(id){
console.log(id,this)
}
}
class Bus extends Vehicle{}
funciton Person(){}
class Engineer extends Person{}
let b = new Bus();
b.identify('bus')//bus,Bus{}
Bus.identify('bus')//bus,class Bus{}
extends可访问到父类及其原型上的的方法了,那么属性如何访问呢,此时super就出现了。
- super只能在派生类使用,并且仅限在类构造函数和静态方法中使用,会自动调用类构造函数和静态方法。在类构造函数中使用super可以调用父类构造函数,相当于“子.call(this,name)”。
- 只要是派生类继承extends中显示定义了构造函数中,必须加上super(),super()负责初始化this,所以this的引用得再super()之后。
class Person {
constructor(name, age) {
this.name = name; //this代表的是实例对象
this.age = age;
}
say() {
return "我的名字叫" + this.name + "今年" + this.age + "岁了";
}
static identify() {
console.log("person");
}
}
class Student extends Person {
constructor(name,age) {
//1.super在派生类的构造函数中使用,可调用父类构造函数,初始化this对象
super(name,age);
console.log(this);//Student{name: 'Mike', age: 22}
}
static identify() {
//2.super在静态方法中使用,可调用父类静态方法
super.identify();
}
}
var s = new Student("laotie", 22);
console.log(s.say());
//验证super在静态方法中的使用
Student.identify();//person
看一个例子
class Person {
constructor(name) {
this.name = name;
}
}
class student extends Person {}
console.log(new student("Mike"));//student {name: 'Mike'}
为什么student能够自动传参到Person呢?因为此处student没有定义类构造函数,实例化时会默认调用super,并且传入所有传给派生类的参数。