对象、原型、继承、类

118 阅读13分钟

对象、原型、继承、类


参考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);将一个或者多个对象的所有可枚举属性复制到目标对象上。实际上是浅复制。

访问的属性不在对象中,那么就会进一步访问对象的原型链。

语法糖:

  1. 属性值简写:
let name = "Mike";
let person = {name:name};
//属性名和值相等时,可以简写一个,即等价于
let person = {name}
  1. 如果想要使用变量作为属性,不能直接用字面量来动态命名属性,得用[ ]。
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'}
  1. 简写方法名

对象定义方法时,通常是一个方法名,冒号,然后一个匿名函数表达式。可简写为:

let person = {
  sayName:function(name){
    console.log(name);
  }
}
//上述标准形式可简写为:
let person = {
  sayName(name){
    console.log(name);
  }
}
  1. 对象解构
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操作符调用构造函数,具体包含以下步骤:

  1. 创建一个新对象;
  2. 将对象的prototype属性被赋值为构造函数的prototype属性;
  3. 将构造函数内部的this指向这个新对象
  4. 执行构造函数内部的代码(给对象增添属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

原型模式

构造函数都有一个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']

问题:

  1. 原型中的引用值会在所有实例之间共享
  2. 子类型实例化时不能给父类传参

盗用构造函数(经典继承)

如果子类在构造函数中调用父类构造函数,那么就会运行父构造函数中的所有初始化代码,每个实例都会有自己的属性。

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上不必要也用不到的属性,效率更高。此为引用类型继承的最佳模式。

1641457243(1).png

类定义有两种:类声明和类表达式。

class Person{};
const Animal = class{};

与函数区别:

  1. 函数声明可以提升,但类定义不行。
  2. 函数受函数作用域限制,类受块作用域限制。

类可包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。

类构造函数

new实例化类的时候,等于使用new调用其构造函数。

class Person {
  constructor() {
    console.log("nice");
  }
}
let p = new Person();//nice
  1. 当构造函数有返回值,且返回的其他对象而不是this对象时,其new创建的对象不会通过instanceof操作符检测出和类有关联,因为对象的原型指针没有被修改。
  2. 调用类构造函数必须new,没有会报错。而普通构造函数不new时候,会以全局的this作为内部对象。
class Person {
  constructor() {
    console.log("nice");
  }
}
//用类类创建实例
let p = new Person();
//实例直接调用constructor会报错
p.constructor()//typeError
//调用类构造函数必须使用new
//用类构造函数创建实例
let p1 = new p.constructor()
  1. 类标识符有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

实例、原型和类成员

  1. 类的每一个实例都对应一个成员对象,所有成员不会在原型上共享。都是一个独立的个体,不会信息共享。
  2. 那么,怎么实现实例间共享方法呢,就是在类块中定义方法,那么此方法就会成为类原型上的方法。日常一般方法都是用来实例共用的,会定义在类块中。
  3. 此外,不能在类块中给原型添加原始值或对象作为成员数据,会报错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

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

  1. 静态类方法,这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个。
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
  1. 在类原型和类本身上可定义生成器方法

继承

类可以继承类,也可以继承普通构造函数。用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,并且传入所有传给派生类的参数。