05深入JS面向对象

83 阅读29分钟

面向对象

编程是对现实世界的抽象,而面向对象是对现实抽象的一种方式。

对象是JavaScript中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物:

  1. 比如我们可以描述一辆车:Car,具有颜色(color)、速度(speed)、品牌(brand)、价格(price),行驶(travel)等 等;
  2. 比如我们可以描述一个人:Person,具有姓名(name)、年龄(age)、身高(height),吃东西(eat)、跑步(run) 等等;

用对象来描述事物,更有利于我们将现实的事物,抽离成代码中某个数据结构:

  1. 所以有一些编程语言就是纯面向对象的编程语言,比Java;
  2. 你在实现任何现实抽象时都需要先创建一个类,根据类再去创建对象;

JS中的面向对象

JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:

  1. JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成;
  2. key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;
  3. 如果值是一个函数,那么我们可以称之为是对象的方法;

创建对象的两种方式

早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象,这是因为早期很多JavaScript开发者是从Java过来的,它们也更习惯于Java中通过new的方式创建一个对象。

// 创建一个对象,对某一个人进行抽象(描述)
var obj1 = new Object(); // 将obj当做构造函数,创建一个空对象 {}
obj1.name = 'cs';
obj1.age = 18;
obj1.height = 1.88;
obj1.running = function () {
  console.log('跑步');
}

后来很多开发者为了方便起见,都是直接通过字面量的形式来创建对象,这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来;

// 字面量的方式
var obj2 = {
  name:'cs2',
  age:20,
  height:1.80,
  eatting:function () {
    console.log('eating吃饭');
  }
} 

对属性操作的控制

var obj = {
  name:'cs2',
  age:20,
  height:1.80,
  eatting:function () {
    console.log('eating吃饭');
  }
} 

// 获取属性的值
console.log(obj.name);
// 删除属性
delete obj.age
// 添加属性
obj.sex = '男';
// 修改属性的值
obj.name = 'cs2'

在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的,但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个 属性是否在for-in遍历的时候被遍历出来呢?

我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符,通过属性描述符可以精准的添加或修改对象的属性,属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改。

Object.defineProperty(obj,'name',{
  //很多的配置
})

Object.defineProperty(obj,prop,descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此 对象。

可接收三个参数:参数一:对哪一个对象做操作;参数二:对该对象的哪个一个属性操作;参数三:要定义或修改的属性描述符(是一个对象)。

返回值:被传递给函数的对象。

属性描述符分类

属性描述符的类型有两种:

  1. 数据属性(Data Properties)描述符(Descriptor);
  2. 存取属性(Accessor访问器 Properties)描述符(Descriptor);
configurable(可配置)enumerable(可枚举)valuewriteablegetset
数据描述符可以可以可以可以不可以不可以
存取描述符可以可以不可以不可以可以可以

1 数据描述符

  1. Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
    • 当我们直接在一个对象上定义某个属性时,这个属性的Configurable为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的Configurable默认为false;
  2. Enumerable:表示属性是否可以通过for-in或者Object.keys()返回该属性;
    • 当我们直接在一个对象上定义某个属性时,这个属性的Enumerable为true。
    • 当我们通过属性描述符定义一个属性时,这个属性的Enumerable默认为false。
  3. Writable:表示是否可以修改属性的值。
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true。
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false。
  4. value:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改。
    • 默认情况下这个值是undefined。
Object.defineProperty(obj,'sex',{
  // configurable / false:deledt是删除不掉的,重新写一个新的属性描述符也是不行的
  configurable:true,
  // enumerable /true:在对象中能看见,false:可以获取值,但是对象里面看不见,Object.keys
  enumerable:true, 
  // writable/ true:值可以被修改,false:值不可以被修改。
  writable:true,
  // 读取属性时返回的值。
  value:'男'
})

2 存取属性描述符

数据数据描述符有如下四个特性:

  1. Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
    • 当我们直接在一个对象上定义某个属性时,这个属性的Configurable为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的Configurable默认为false;
  2. Enumerable:表示属性是否可以通过for-in或者Object.keys()返回该属性;
    • 当我们直接在一个对象上定义某个属性时,这个属性的Enumerable为true。
    • 当我们通过属性描述符定义一个属性时,这个属性的Enumerable默认为false。
  3. get:获取属性时会执行的函数。默认为undefined。
  4. set:设置属性时会执行的函数。默认为undefined。
var obj = {
  name:'cs2',
  age:20,
  _sex:'男',
  eatting:function () {
    console.log('eating吃饭');
  }
} 

// 存取属性描述符
Object.defineProperty(obj,'sex',{
  configurable:true,
  enumerable:true,
  get(){
    return this._sex;
  },
  set(value){
    this._sex = value;
  }
})
obj.sex = '女';
console.log(obj.sex);
console.log(obj);

使用场景:隐藏某一个私有属性,不希望直接被外界使用和赋值。如果希望截取某一个属性它访问和设置值的过程中,也会使用到存储属性描述符。

3 同时定义多个属性

var obj = {
  // 私有属性,js里面没有严格意义的私有属性,而是社区的一种默认规范。
  _age:18
};

// 参数一:哪一个对象? 参数二:属性描述符
Object.defineProperties(obj,{
  name:{
    enumerable:true,
    configurable:true,
    writable:true,
    value:'cs'
  },
  age:{
    enumerable:true,
    get(){
      return this._age
    },
    set(newValue,oldValue){
      this._age = newValue
    }
  }
})

console.log(obj);

当然还有另外的一种写法:

var obj = {
  _age:18,
  get age(){
    return this._age;
  },
  set age(val){
    this._age = val
  }
};

对象方法补充

获取对象的属性描述符

getOwnPropertyDescriptorgetOwnPropertyDescriptors

// 获取某一个特定属性的,属性描述符
Object.getOwnPropertyDescriptor(obj,'name');
// { value: 'cs', writable: true, enumerable: true, configurable: true }

// 获取对象所有属性的属性描述符。
Object.getOwnPropertyDescriptors(obj);

Object的方法对于对象的限制

var obj = {
  name:'cs',
  age:18,
}

// 禁止添加新的属性 参数:操作的对象名
Object.preventExtensions(obj);
obj.heigth = 18;//添加不进去

// 禁止对象 配置/删除 里面的属性
Object.seal(obj);
delete obj.name;

// 不可以修改属性的值,只会冻结一级属性。
Object.freeze(obj); 
obj.name = '111';

console.log(obj

工厂函数

如果我们现在希望创建一系列的对象:比如Person对象

  1. 包括张三、李四、王五、李雷等等,他们的信息各不相同;
  2. 那么采用什么方式来创建比较好呢?

前面已经了解了创建对象的两种方式:

  1. new Object方式;
  2. 字面量创建的方式;

这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;

我们可以想到的一种创建对象的方式:工厂模式。

  • 工厂模式其实是一种常见的设计模式;
  • 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;
function createPerson(name,age,height) {
  var p = {};
  p.name = name;
  p.age = age;
  p.height = height;
  p.eating = function(){
    console.log(this.name);
  }

  return p
}

var p1 = createPerson('张三',20,1.88);
var p2 = createPerson('李四',22,2.00);
var p3 = createPerson('王五',18,1.75);

p1.eating()
p2.eating()
p3.eating()

对应的缺点:我们无法看到对象具体的一个类型,比如上面全部都是Preson类型,但是我们得不到回馈,因为显示的全都是Object类型。

构造函数

工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型,所以我们看一下另外一种模式:构造函数的方式;

什么是构造函数?

  1. 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;
  2. 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
  3. 但是JavaScript中的构造函数有点不太一样;

avaScript中的构造函数是怎么样的?

  1. 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
  2. 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;
function Person() {
  console.log('你好');
}

// Person是一个普通的函数
Person();

// 如果通过new去调用,那么Person 就是一个构造函数
new Person()

小结:构造函数和普通函数没有任何的区别,和它的调用方式有关系,如果你是通过new来进行调用的,那么这个函数就是构造函数。

new到底干了什么?

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行函数的内部代码(函数体代码);
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;
    • 如果返回值是基础数据类型,则忽略返回值;
    • 如果返回值是引用数据类型,则使用return 的返回,也就是new操作符无效;
// 不用()也是可以这样调用构造函数的,为啥需要()那是因为需要传值。
new Person

对象的原型

1 JavaScript当中每个对象都有一个特殊的内置属性 __proto__([[prototype]]),这个特殊的对象可以指向另外一个对象。

2 那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

  • 答案是有的,只要是对象都会有这样的一个内置属性;
  • 获取的方式有两种:
    1. 通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问 题);
    2. 方式二:通过 Object.getPrototypeOf 方法可以获取到;
var p = {name:'cs2'};
var p = {name:'cs2',__proto__ : {}}

当你写了一个对象后,会默认给你添加这么一个属性,我们称之为隐式原型

const obj = {
  names: "cs",
};
console.log(obj.__proto__);//图片如下所示
console.log(Object.getPrototypeOf(obj));

image-20220822195152666

这个原型对象有什么作用?

  1. 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
  2. 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
  3. 如果对象中没有改属性,那么会访问对象__proto__内置属性指向的对象上的属性;

函数的原型(prototype)

函数也是一个对象。所以函数作为对象来说,它也是有__proto__的。

function Foo() {
}
console.log(foo.__proto__);

函数同时还会多出来一个显示原型的属性,没有浏览器兼容问题。对于某一个对象来说,它只有__proto__而没有prototype,只是因为它是一个函数,才有了prototype这个特殊的属性。

Foo.prototype // 存在一个对象
var obj = {};
console.log(obj.prototype); //obj不存在这个属性

这个时候我们再去理解,new操作符的第二个,将new出来的对象的隐式原型指向了构造函数的显示原型

image-20220822203341801

var f1 = new Foo();
var f2 = new Foo();
console.log(f1.__proto__ === f2.__proto__);//true
console.log(f1.__proto__ === Foo.prototype);//true

这个时候能够发现,new出来的两个对象,他们的__proto__是相等的,意思就是指向了同一个对象。

构造函数的使用

function Person(name,age,address) {
  // 此时的this就是p1 第三个步骤 改变this指向
  this.name = name;
  this.age = age;
  this.address = address;
  this.running = function () {
    console.log(this.name + '在跑步');
  }
  this.eating = function () {
    console.log(this.name + '在吃饭');
  }
}
const p1 = new Stus("cs1", 20, 1.88);
const p2 = new Stus("cs2", 21, 1.88);
p1.running()

约定规范:构造函数首字母大写。主要是为了区别普通函数和构造函数。

构造函数的缺点:它在于我们需要为每个对象的函数去创建一个函数对象实例,简单来说就是每一次的调用,都是在堆内存中开辟了一块新的内存。

当我们创建p1的时候,会给他创建eatingrunning两个函数对象,在创建p2的时候,同样也是这样的操作。

但是由于eatingrunning的函数体,所保存的代码都是一样的,所以没有必要给每一个对象,都创建这样的函数对象。

那么我们是不是可以将一些方法,添加到原型对象上面?来减少内存的分配。

2 原型对象添加一些方法。

Stus.prototype.running = function () {
	console.log(`${this.name}跑步`);
};

我们就会使用给原型对象上面添加方法的形式,来简化上面的写法。

重写原型对象

1 constructor的简单介绍

function Foo() {
}
console.log( Object.getOwnPropertyDescriptors(Foo.prototype));
// 原型上面的所有属性
{
  constructor: {
    value: [Function: Foo],
    writable: true,
    enumerable: false,
    configurable: true
  }
}

Foo.prototype有一个叫做 constructor的属性,这个属性会指向构造函数的本身。

function Foo() {
  
}
console.log(Foo.prototype.constructor.name);
// 获取函数名:Foo.name  获取参数的长度 Foo.length

所以我们可以通过constructor.name的形式,获取到函数的名字。

2 原型对象的重写

function Foo() {
  
}
Foo.prototype = {
  name:'cs',
  age:18,
  height:1.88
}

当我们这样重写原型对象的时候,原来的原型对象就会被销毁(没有引用),Foo.prototype会重新指向你新建的对象,那么这个时候我们就会发现一个问题?constructor好像是没有了?

<script>
  Foo.prototype = {
    constructor:Foo,
    name:'cs',
    age:18,
    height:1.88
	}
	// 但是原本的 constructor 是不可被枚举的,所以我们可以通过另外一种方式进行添加
  Object.defineProperty(Foo.prototype,'constructor',{
    enumerable:false,
    writable:true,
    configurable:true,
    value:Foo
  })
</script>

那么我们就可以通过这样的方式,给重写的原型对象,添加上constructor属性了,当然使用第二种方法。

构造函数和原型组合

function Person(name,age,address) {
  this.name = name;
  this.age = age;
  this.address = address;
}

Person.prototype.eating = function () {
  // 这里的this和调用有关系,p1调用就是p1对象
  console.log(this.name + '吃东西');
}
var p1 = new Person('张三',18,'西安');
var p2 = new Person('李四',22,'深圳');


p1.eating()

image-20220822211921982

问题解答:为啥name、age不放在原型上面呢? 这是因为如果将这些放在原型上面的话,那就会出现一个问题,p1.name 就name修改为张三,那么p2呢?将name修改为李四,这个时候p1.name不就改变了吗?对于函数来说因为里面的函数体代码都是一样的。

可枚举的补充

在浏览器中我们能够看到通过Object.defineProperty创建的对象即使enumberable为false,这是因为浏览器为了方便我们进行调试的结果,我们仔细观察后就会发现其实,这个数据和其他数据的颜色是不一样的。

JS中的类和对象

function Person(){

}
var p1 = new Person();
var p2 = new Person();

当我们编写如下代码的时候,我们会如何来称呼这个Person呢?在JS中Person应该被称之为是一个构造函数;从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们创建出来对象p1、p2;如果从面向对象的编程范式角度来看,Person确实是可以称之为类的;

面向对象的特性

面向对象有三大特性:封装、继承、多态

  1. 封装:我们将属性和方法封装到一个类(构造函数)中,可以称之为封装的过程;
  2. 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);而且继承也是多态的前提。
  3. 多态:不同的对象在执行时表现出不同的形态;
  4. 抽象:将现实的事物抽象为代码的一个过程。

那么继承是做什么呢?继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

JavaScript原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取。

那么什么地方是原型链的尽头呢?其实就是Object.prototype,这就是我们的原型链的尽头,而Object.prototype__proto__则是nullObject其实就是一个构造函数。

对于构造函数来说,原型链的顶层依旧是Object的原型对象(Object.prototype),总体来说:构造函数有显示原型对象,实例化的对象有隐式原型对象,构造函数的显示原型和实例化的隐式原型,指向同一个原型对象(Person.prototype === p1.__proto__)。然后构造函数的prototype对象的隐式原型指向Object的原型对象。

原型链的继承

// 父类 公共属性和方法
function Person() {
  this.name ='cs';
  this.age = 18
}

Person.prototype.eating = function () {
  console.log(this.name + 'eating');
}

// 子类 学生
function Students() {
  this.son = 1818;
}

//原型链的继承
// Students.prototype = Person.protopyte; 
// 不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改
Students.prototype = new Person();

Students.prototype.studying = function () {
  console.log(this.name + '学习');
}

// 创建学生
var stu1 = new Students();

stu1.eating();//cseating
stu1.studying();//cs学习

小结:通过Students.prototype = new Person()来实现原型链的继承,需要注意的是顺序问题。

  1. Students.prototype = new Person()必须添加在Students.prototype.studying的前面,因为是在改变原型链指向后添加的,如果顺序写错,那么Students.prototype.studying就会访问不到了,因为Students之前的原型对象,已经没有引用了,所以会被回收。
  2. 看图能够解释一切。

还有一个问题

Students.prototype = Person.protopyte;
Students.prototype = new Person();

为什么不采取第一种写法呢?当时我的想法也是这样的,假如说当你给Students的原型上添加styding方法的时候,这是直接添加到了Person的原型上,如果再来一个Teacher继承Person呢?这个时候Teacher身上是不是也会有这个方法呢?如果继承的很多,那么所有的东西是不是都共享了?那么这还是继承吗?

缺点

1 继承过来的属性是直接看不到,需要通过原型来进行查找。

2 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;

// 父类 公共属性和方法
function Person() {
  this.name ='cs';
  this.age = 18;
  this.friends = [];
}
var stu1 = new Students();
var stu2 = new Students();
stu1.friends.push('李四');

console.log(stu1.friends);
console.log(stu2.friends);

当你给stu1的friends里面姓名的时候,stu2也会添加上,这显然是不合情理的,我有了一个朋友,你一定会有吗?而应该是相互独立的。

3 不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);

构造函数继承

function Parent(name) {
    this.name = name;
}
Parent.prototype.getName = function() {
    return this.name;
}
function Child() {
    Parent.call(this, 'zhangsan')   
    // 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上
}

//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()      // 报错,找不到getName(), 构造函数继承的方式继承不到父类原型上的属性和方法

构造函数继承的缺点:

继承不到父类原型上的属性和方法。

组合式继承

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函 数或者称之为经典继承或者称之为伪造对象):

  1. steal是偷窃、剽窃的意思,但是这里可以翻译成借用;

借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数

  1. 因为函数可以在任意的时刻被调用;
  2. 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;
// 父类 公共属性和方法
function Person(name,age,friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
}

Person.prototype.eating = function () {
  console.log(this.name + 'eating');
}

// 子类 学生
function Students(name,age,friends,sno) {
  // 所谓的构造函数的继承
  Person.call(this,name,age,friends);//new Students 的 this
  this.son = sno;
}

Students.prototype = new Person();

Students.prototype.studying = function () {
  console.log(this.name + '学习');
}

// 创建学生
var stu1 = new Students('张三',18,'cs','1818001');
// var stu2 = new Students();

console.log(stu1);

当我们这样去做的时候,通过原型链继承的缺点已经是被我们成功的解决了,当然这种方法叫做组合继承(就是构造函数和原型链一起来),

缺点:

  1. 组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。
    • 一次在创建子类原型的时候
    • 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
  2. 所有的子类实例事实上会拥有两份父类的属性
    • 份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.__proto__里面);
    • 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;

由于调用了两次的父类,而且stu的原型对象上,会多出很多属性,而且这些属性的值都是undefined

原型式继承

原型式继承的渊源:

  1. 这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的 一篇文章说起: Prototypal Inheritance in JavaScript(在JS中使用原型式继承)。
  2. 在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的。
  3. 为了理解这种方式,我们先再次回顾一下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法。

首先要明确一下,原型链和借构造函数的缺点,那就是最少调用了两次父类,那么我们有没有一种办法能解决呢

  1. 首先要有一个对象。
  2. 这个对象的隐式原型指向,父类的显示原型。
  3. 子类的隐式原型指向这个对象。
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.eat = function () {
  console.log("eat");
};
function Stus(name, age, sno, score) {
  Person.call(this, name, age);
  this.sno = sno;
  this.score = score;
}
// 方法一
function createObj(o) {
  var obj = {};
  Object.setPrototypeOf(obj, o);
  return obj;
}
Stus.prototype = createObj(Person.prototype); //createObj,返回的是一个新对象

Stus.prototype.study = function () {
  console.log("study");
};

var s1 = new Stus("cs", 18, 1002546, 80);

当然现在的实现比较简单,之前是因为没有setPrototypeOf这个函数的,那么没有这个api的时候是怎样继承的呢?

function crateobject(o) {
  function Fn(){};
  Fn.prototype = o; // 将Fn的显示原型指向了 obj
  var newObj = new Fn();  // 相当于 newObj.__proto__ = o;
  return newObj;
}

最新还有更加简单的办法,效果则是和上面的一模一样。

var info = Object.create(Person.prototype); // 将obj,作为新创建出来对象的原型
Stus.prototype = info;

寄生式继承

寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;

寄生式继承的思路是结合原型类继承和工厂模式的一种方式;

即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;

var Person = {
  running:function () {
    console.log('running');
  }
}

// 期望学生对象继承自Person对象

// 方法一:
var student = Object.create(Person);
student.running();
// 弊端 想要扩展属性的时候,需要用下面的方法,但是如果是很多的学生呢?那就太麻烦了
student.eating = function () {};
student.name = 'cs';

通过工厂的模式进行改进。

function createStu(person,name) {
  var stu = Object.create(person);
  stu.name = name;
  stu.studing = function () {
    console.log('studing');
  }
  return stu;
}

var stu1 = createStu(Person,'cs');
var stu2 = createStu(Person,'cs2');
console.log(stu1);
console.log(stu2);

这样创建出来的缺点:首先类型还是不明确,其次就是studing会给每个属性都添加上。

寄生组合式继承

最后方案:寄生组合式继承。

function Person(name,age) {
  this.name = name;
  this.age = age;
}
Person.prototype.eating = function () {
  console.log('吃饭');
}
Person.prototype.sleeping = function () {
  console.log('睡觉');
}

function Students(name,age,sno) {
  Person.call(this,name,age)
  this.sno = sno;
}
Students.prototype = Object.create(Person.prototype)
Students.prototype.stuying = function () {
  console.log('学习');
}

var stu1 = new Students('张三',18,11111);
var stu2 = new Students('李四',20,11112);

console.log(stu1);
console.log(stu2);

这种方法可以说是目前最成功的解决方式,可以传递参数,同时解决了组合式继承,调用两次父构造函数的问题,也不会出现父类里面那些属性值都是undefined的表现。

Students.prototype = Object.create(Person.prototype);
Object.defineProperty(Students.prototype,'constructor',{
  enumerable:false,
  configurable:true,
  writable:true,
  value:Students
})
Students.prototype.stuying = function () {
  console.log('学习');
}

当然在浏览器里面进行展示的时候是没有问题,但是在控制台打印的时候,我们会发现new Students出来的类型却是Person类型,那么我们就可以通过上面的方式进行改变了,当然还可以继续进行一个简化,因为如果说我们还有一个Teacher来继承Perosn,那么上面的代码是不是要重复的写一遍呢?所以我们可以去封装一个函数。

function inherit(subType,superType) {
  subType.prototype = Object.create(superType.prototype);
  Object.defineProperty(subType.prototype,'constructor',{
    enumerable:false,
    configurable:true,
    writable:true,
    value:subType
  })
}
function Person(name,age) {
  this.name = name;
  this.age = age;
}
Person.prototype.eating = function () {
  console.log('吃饭');
}
Person.prototype.sleeping = function () {
  console.log('睡觉');
}
function Students(name,age,sno) {
  Person.call(this,name,age)
  this.sno = sno;
}
inherit(Students,Person);// 参数一:子类,参数二:父类
Students.prototype.stuying = function () {
  console.log('学习');
}
var stu1 = new Students('张三',18,11111);
var stu2 = new Students('李四',20,11112);
console.log(stu1);
console.log(stu2);

到时候我们只需要将子类和父类穿进去就OK了,这样写起来是不是方便了很多呢?

原型关系合集

function Function(){

}
// 相当于是由,下面这个创建出来的。
var Function = new Function()

class定义类

我们会发现,按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类;但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;

定义类的两种方式

class Person {

}

var aniamls = class {
  
}
// 我们都知道构造函数(类)都有显示原型
console.log(Person.prototype.__proto__);// Object的显示原型对象
console.log(typeof Person); // function

我们分别称之为:类的声明 和 类表达式。

类和构造函数的区别

class Person {

}

var p1 = new Person();

console.log(p1.__proto__ === Person.prototype); //true

当我们以这种方式去进行调用的时候,我们就会发现其实和构造函数好像是差不多的。也是支持new关键字的。

类的constructor

如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?

  1. 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor
  2. 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
  3. 每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;

当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:

  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行构造函数的内部代码(函数体代码);
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;
class Person {
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  // 固定的写法
  running(){
    console.log(this.name + '在跑步');
  }
  running = () => {}
}
const p1 = new Person("Cs", 18);
console.log(Person.prototype === p1.__proto__);//true
console.log(Person.prototype.constructor);//也是指向了Person
// Person里面定义的函数,是放在了Person的prototype上面。

不同点:

function Person1 (){};
class Person2 {};
// p1可以作为普通的函数调用,但是Person2只能通过new去调用

get和set

const obj = {
  _name: "cs",
  get name() {
    return this._name;
  },
  set name(value) {
    this._name = value;
  },
};
// 类里面的写法
class Person {
  constructor(name, age) {
    this._name = name;
    this.age = age;
  }
  get name() {
    return this._name;
  }
  set name(val) {
    this._name = val;
  }
}

类的静态方法

对于ES5的时候,我们给某个函数原型所添加的方法,只能通过new实例对象来进行调用,而构造函数本身是调用不了的。在class定义类之后,可以这么写:在方面前面添加static;静态方法也是可以继承过来的。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static create() {
    console.log("create");
  }
}
Person.create();//create

子类是可以继承父类的静态方法的也可以进行重写,但是静态方法只能是子类调用,子类的实例对象是调用不了的。

Class的继承

普通类的继承

class Person {
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
}
class Stus extends Person {
  constructor(name,age,sno){
    super(name,age);
    this.sno = sno;
  }
}
// 通过 super来调用父类的 constructor,将参数传递过去。
var s1 = new Stus()

这样我们便可以轻松的实现继承了,下面我们再来说一些方法的重写

class Person {
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  eatting(){
    console.log('父类的吃饭方法');
  }
}

class Stus extends Person {
  constructor(name,age,sno){
    super(name,age);
    this.sno = sno;
  }
  eatting(){
    console.log('子类的吃饭方法');
  }
}

var s1 = new Stus();
s1.eatting();// 子类的方法

这个时候我们会执行子类的方法,而不是父类的方法,我们称之为方法的重写。

class Stus extends Person {
  constructor(name,age,sno){
    super(name,age);
    this.sno = sno;
  }
  eatting(){
    super.eatting();//调用父类的方法
    console.log('子类的吃饭方法');
  }
}

当你在调用子类的某个方法的时候,是可以调用父类的方法的,那么这样的情况下,父类和子类的eatting方法都会被调用。

内置类的继承

class CSArray extends Array {
  lastItem() {
    return this[this.length - 1];//获取数组元素 arr[index]
  }
}

const c1 = new CSArray(1, 2, 3);
console.log(c1); //CSArray(3) [ 1, 2, 3 ]
console.log(c1.lastItem());// 3

super的使用

super可以在三个地方进行使用:

  1. 子类的constructor里面。
  2. 子类的某一个方法里面,调用父类的方法。
  3. 子类的某一个静态方法里面,调用父类的静态方法。

类的混入mixin

JavaScript中的类只支持单继承:也就是只能有一个父类

但是在我们的某些应用场景下,却需要实现在一个类中添加很多相似功能的时候,就可以使用混入(mixin);

class Person {
 eat(){
    console.log('eat');
  }
}
class Games{
  tennis(){
    console.log("play tennis");
  }
}

class Stus {
  study(){
    console.log("study");
  }
}

// 如果我们想要继承多个类里面的属性
function mixinPerson(BaseClass){
  return class extends BaseClass{
    // 在这里写想要继承的方法
    eat(){
    	console.log('eat');
  	}
  }
}
function mixinGames(BaseClass){
  return class extends BaseClass{
    // 在这里写想要继承的方法
    tennis(){
    	console.log("play tennis");
  	}
  }
}

class newStus extedns mixinPerson(mixinGames(Stus)){
  
}
const ns1 = new ();//这个时候ns1就有了三个类的方法了

多态

我们所知:面向对象的三大特性:封装、继承、多态。

面向对象

那么JavaScript有多态吗?

  1. 维基百科:多态(polymorphism)指的是为不同的数据类型的实体,提供了统一的接口,或者使用一个单一的符号来表示多个不同的类型。
  2. 简单来说:不同的数据类型进行同一个操作,表现出不同的行为,这个就是多态的体现。

那么从上面的定义来看,JavaScript是一定存在着多态的。

class Shape {
  getArea(){}
}

class Circle extends Shape {
  getArea(){
    return 2000
  }
}

// Rectangle:矩形、长方形
class Rectangle extends Shape {
  getArea(){
    return 1000
  }
}

var c = new Circle();
var r = new Rectangle();

// 传入的类型必须是Shape类型,而我们的 c 和 r都是继承自Shape类型
function calcArea(shape:Shape){
  console.log(shape.getArea());
}

calcArea(c);//2000
calcArea(r);//1000

这就是多态比较简单的一种表现,所以我们之前所说到过,继承其实就是多态的前提,而且必须有子类重写父类的方法,必须有父类的引用指向子类的对象。

JS的角度

那么从JS的角度出发呢?

//为不同的数据类型的实体,提供了统一的接口
function foo (a1,a2){
  return a1 + a2;
}
foo(1,2);
foo("3","4");

那么这种不就是为不同的数据类型,提供了统一的接口吗?

// 使用单一的符号,表示多种不同的类型
var foo = 123;
foo = "hello";

这种也算是,使用单一的符号,表示多种不同的数据类型。

手写函数

function foo() {};
	console.log(foo.__proto__ === Function.prototype); //true
</script>

首先我们明确几点:

  • 当我们通过foo.apply()这样去调取函数的时候,相当于是当成对象来使用了,而这些方法则保存在foo.__proto__的对象里面。
  • 我们添加到Function.prototype上面的方法,可以被所有函数进行调用。

1 手写apply方法

Function.prototype.CSapply = function (ctx, args) {
  ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
  ctx.fn = this;
  Object.defineProperty(ctx, "fn", {
    configurable: true,
    enumerable: false,
    writable: true,
    value: this,
  });
  ctx.fn(...args);
  delete ctx.fn;
};

2 手写bind方法

Function.prototype.CSbind = function (ctx, ...firstArgs) {
  ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
  ctx.fn = this;
  Object.defineProperty(ctx, "fn", {
    configurable: true,
    enumerable: false,
    writable: true,
    value: this,
  });
  return (...twoArgs) => {
    const allArgs = [...firstArgs, ...twoArgs];
    ctx.fn(...allArgs);
  };
};