JavaScript中的代码复用——this、对象、类(2)(伪类的旅程)

前言

一共有三篇,这一篇写在JS中运用面向对象思想来编程。因为typescript不熟悉所以这部分在实际运用后再回来补。

内容来源于《你不知道的js》《阮一峰ES6入门》《JavaScript语言精粹》《JavaScript高级程序设计》《JavaScript设计模式》《JavaScript模式》《Typescript官网》《MDN web文档》
本博客没有什么有价值的知识,仅作总结梳理之用,初学者可以看看。

原型链

JS中有两条链,一条作用域链,闭包与this的指向与此链有关;一条是原型链,方法属性的查找和委托与此链有关。

如下面代码所示

let foo  = {
  a: 2
}
let bar = {} 
bar.__proto__ = foo
bar.a //? 2
bar.__proto__=== foo //? true 
bar.hasOwnProperty('a') //? false 没有a但是可以调用a
foo.hasOwnProperty('a') //? true

// __proto__它的含义是原型指向,但不是标准属性不推荐使用,应该用
Object.setPrototypeOf()(写操作)、
Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替

新建一个空白的bar对象,并将原型链至foo。如下图所示


原型链示意图

在js中任何对象的原型链最终指向于Object.prototype(它自身指向null)。在JS中,当访问对象属性或者调用函数时,如果未在当前对象找到,那么将会沿着原型链向上追溯,譬如bar.hasOwnProperty方法,hasOwnProperty是定义在Object.prototype上的,但是bar却可以调用,同样的bar也可以访问foo的属性a,值为2,但是bar.hasOwnProperty('a')可以看到false,说明其自身并不具有这个属性。

类的一些概念

因为这一篇是写类的,所以用词用语都是下方的用语,在js中是不准确的也是和实际情况有歧义的。但是还是要这么说。

  • 类(Class): 定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(Object):类的实例,通常通过 new生成,是具体的事物
  • 面向对象(OOP)三大特征:封装、继承、多态
  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口,外界调用端不需要也不可能知道细节,就能通过对外的接口来访问对象,同时也保证外界无法任意更改对象内不的数据
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。
  • 存取器(getter&&setter):用以改变属性的读取和赋值行为
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如public表示共有属性或方法
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,本身不允许被实例化。抽象类中的抽象方法必须在子类中被实现
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口。

new

先看看new的时候发生了什么事情

function FunOne(a) {
  this.a = a
  this.b = function () {
    return 3
  }
}
let ObjOne = new FunOne(1)
ObjOne.a //?1

按照面向对象类的说法是,这个FunOne是构造函数,而new创建了一个新实例,也就是实例化的过程。
不过并不是这样,FunOne只是一个普通的函数和其他任何一个函数都没有什么区别,
函数可以立即执行可以当作返回值可以被函数或对象调用,这里就是new调用,FunOne是个普通的函数当它new调用时就被构造调用了。不过为了行文方便还是称之为构造函数,因为类总是要有构造函数的,那就假装它是吧。


new调用

记录下new调用的过程
new做了四件事:

  1. 一个全新的对象会凭空创建
  2. 这个新构建的对象被加入原型链
  3. 这个新构建对象被设置为函数调用的this绑定(即是this指向这个新对象)然后执行构造函数中的语句
  4. 返回这个对象
    可以自己写个函数实现new的功能(毫无必要仅仅演示)
function build(name) {
 //新建一个对象
  let F = {}
//设置原型链
  F.__proto__ = Foo.prototype
//运行构造函数内语句
  F.name = name
//返回这个对象
  return F
}
function Foo(name) {
  this.name = name
}
let foo1 = build('Mike') //起到了和new一样的作用
foo1.name //?Mike

每一个函数都有一个prototype属性,且这个属性的值是对象,这个对象除了从构造函数复制过去的所有属性之外,还有个construcor属性且值为此构造函数,也就是这俩函数和对象互相为对方一个属性的值。如果查询一下会发现这个这个对象在控制台会输出FunOne{}也就是和构造函数同名了,不过为了不混淆,下文继续用Funone.prototype表示这个对象,写为prototype对象。原型对象定义为这个对象的__proto__指向的那个对象,名词指代清楚才能不搞混。


function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['Shelby','Court']
}
Person.prototype = {
  constructor: Person, //因为Person.prototype对象被重新定义了所以要在新对象中添加上constructor属性来确认指向
  sayName: function () {
    console.log(this.name)
  }
}
let person1 = new Person('Nicholas', 29, 'Software Engineer')
let person2 = new Person('Greg', 27, 'Doctor')
person1.friends.push('Van')
person1.friends //? [ 'Shelby', 'Court', 'Van' ]
person2.friends //? [ 'Shelby', 'Court' ]
person1.friends === person2.friends //? false
person1.sayName === person2.sayName //? true
person1.hasOwnProperty('sayName') //? false 实例person1并没有sayName方法
Person.prototype.hasOwnProperty('sayName') //? true 在这个prototype对象上

知道了它们的指向了。我们可以这样构造一个对象,虽然没什么用。这个构造方式能成立因为 barObjec.prototype创建并被链接到foo.prototype这个对象上,然后bar可以沿着原型链查找并调用foo.prototype上的construcor属性,且此属性指向foo()函数,最终bar调用了foo函数且函数内this指向为bar。

function foo(name) {
  this.name = name
}
let bar = Object.create(foo.prototype)
bar.constructor('mike')
bar.name //? mike

总结: 我们现在有了构造函数,有了prototype对象可以当作类,也有看new语句当作实例化。离伪装成类又近了一步。

原型模仿继承

伪经典继承

《高程》上的例子

      function SuperType(name){
        this.name = name
        this.colors = ["red", "blue", "green"]
      }
      SuperType.sayColor = function () {
        return 'blue'
      }  
      SuperType.prototype.sayName = function(){
        return 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(){
       return this.age
      }
      let instance1 = new SubType("Nicholas", 29)
      instance1.colors.push("black")
      instance1.colors//"red,blue,green,black"
      instance1.sayName()//"Nicholas";
      instance1.sayAge() //29
      let instance2 = new SubType("Greg", 27);
      instance2.colors //"red,blue,green"
      instance2.sayName(); //"Greg";
      instance2.sayAge(); //27
      SuperType.sayColor() //? 'blue'
      instance1.sayColor() //? not a function
      =========================================================
      Object.getOwnPropertyDescriptors(SuperType.prototype) 
      //?{constructor:SuperType,sayName:fuc}
      Object.getOwnPropertyDescriptors(SubType.prototype)
      //?{construor:SubType,name:undefined,colors:"red,blue,green",sayAge:func}
      Object.getOwnPropertyDescriptors(instance1) 
      //?{name:'Nicolas',colors:"red,blue,green,black",age:'29'}
      Object.getOwnPropertyDescriptors(instance2) 
      //?{name:'Greg',colors:"red,blue,green",age:'27'}

原型继承

先利用SubType.prototype = new SuperType()得到一个对象只有name和colors的SubType.prototype且与SuperType.prototype链接,现在用construcor再连回去连到SubType()上。现在调用new SubType()创建新对象时不仅运行了SubType()内的语句并且用call使SuperType内语句在此环境运行,并且得到一个新的对象拥有name,colors和age属性。instance1的方法分别定义在原型链上方的两个对象上,调用的时候沿链查找。sayColor()SuperType()独有,其他不能调用且SubType都不能继承。
现在有了父类,子类,实例。

extend 函数

这个方法在《高程》里被称为寄生组合方法用的函数名是interitPorototype,这里用extend和interit。思路依然是原型链的链接,将子类与父类链接起来
借用一个空对象然后将child.prototypeparent.child联系起来,下面用的Object.create方法可以少了一次new调用。child.prototype上少了不需要的属性,同样的上文中的也可以如此替换,new F()也是为了少创建个父类的新实例。都是为了减少调用父类的构造函数又起到链接的目的。
其余的就是将原来手工链接的语句放进函数里,基本上就是原来的省代码版,所以原型图不画了

function extend(child,parent) {
  let F = function() {}
  F.prototype = parent.prototype
  child.prototype = new F()
  child.prototype.constructor = child
}

function inherit(child, parent) {
 let F= Object.create(parent.prototype)
 F.constructor = child
 child.prototype = F
}

复制下Typescript官网上由ts中class转译的继承代码,分为两部分,第一部分将子类构造函数与父类构造函数链接起来extendStatics实现继承静态属性,第二部分和上面写法一个意思。

var __extends = (this && this.__extends) || (function () {
  var extendStatics = function (d, b) {
      extendStatics = Object.setPrototypeOf ||
          ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
          function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
      return extendStatics(d, b);
  }
  return function (d, b) {
      extendStatics(d, b);
      function __() { this.constructor = d; }
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  };
})();

ES6中的Class

class是es6新增的,class本质上是语法糖,内在还是原型链。

class的写法

先看看class的写法

class Person {
  constructor(name,age) {
    this.name = name
    this.age = age
  }
  static classMethod() {
    return 'hello'
  }
  sayName() {
    return this.name
  }
}
Person.classProperty = 'hi'
let person1 =new Person('Mike',18)
person1.sayName() //? Mike
Person.classMethod() //? hello
Person.classProperty //? hi

语法很简单原来当作构造函数的里的语句被放在了constructor里面,函数本身的静态方法可以通过static添加,但是静态属性只能在类外添加。(提案可以将实例属性在class中的constructor外添加,且静态属性可以在类内添加。用babel转码可以使用,暂不讨论)。现在来测试一下并且画个原型链图

typeof Person //? functhion
Person === Person.prototype.constructor //? true
可以看出类也只是原型的另一种写法。

再测一下各个函数·对象拥有的属性

person1.classProperty //?undefined
person1.classMethod() //?not a function
Object.getOwnPropertyDescriptors(Person) 
// {classMethod(), classproperty: 'hi',prototype: Person{}} 

Object.getOwnPropertyDescriptors(Person.prototype) 
// {contructor: Person(), sayName()}

Object.getOwnPropertyDescriptors(person1) 
// {name: 'Mike', age:'18', sex'man'}
class
枚举性

类中定义的原型属性都是不可枚举的,和new构造函数写法不同,那个是可枚举的。

定义在Person上的静态属性好好的保存在Person函数上并且实例person1无法调用,person1拥有自己的三个属性由类构造方法里的语句设置,实例可以通过原型链调用Person.prototype上的方法。和原型写法一样,除了枚举性。但是如果想要设置实例共享属性的话,就没办法在类上定义只能在SuperType.prototype上定义,依然要用到原型。

class的继承

为了和《高程》上写的对比所以写相似的

class SuperType {
  constructor (name){
    this.name = name
    this.colors = ["red", "blue", "green"]
  }
  sayName () {
    return this.name
  }
  static sayColor() {
    return 'blue'
  }
}
class SubType extends SuperType {
  constructor(name,age) {
    super(name)
    this.age = age
  }
  sayAge() {
   return this.age
  }
}
let instance1 = new SubType('Nicholas',29)
instance1.colors.push("black")
let instance2 = new SubType('Greg',27)

SubType.sayColor() //? 'blue'
instance1.colors //? ["red", "blue", "green",''black'']
instance1.sayAge() //? 28
instance1.sayName() //? Nicholas

很明显能看出了省了很多代码,
定义在原型上的方法现在可以直接定义在类里,直接用extends继承,也就是链接原型链时,比用new少了一次构造函数调用,且不用被constructor指向迷惑。super起到了和call类似作用。下面检测一下画个原型图看看和《高程》上的有没有区别

Object.getPrototypeOf(SubType.prototype) 
//SuperType{}  即SuperType().prototype

Object.getPrototypeOf(SubType) 
//SuperType()

Object.getOwnPropertyDescriptors(SuperType) 
//{prototype: SuperType{} ,sayColor()}

Object.getOwnPropertyDescriptors(SuperType.prototype) 
//{constructor: SuperType(),sayName()}

Object.getOwnPropertyDescriptors(SubType) 
//{prototype: SubType{}}

Object.getOwnPropertyDescriptors(SubType.prototype) 
//{constructor: SubType(), sayAge()}

Object.getOwnPropertyDescriptors(instance1) 
//{name:'Nicolas',age:'28', colors:["red", "blue", "green","black"]}

Object.getOwnPropertyDescriptors(instance2) 
//{name:'Greg',age:'27', colors:["red", "blue", "green"]}
class继承

原型链的链接和《高程》写法多了一个SubType和SuperType两个函数直接的链接,以往都是直接链到Function上。这样子类能直接“继承”父类的静态方法sayColor(),实例不可调用。

super的使用

在es6中新增了super关键字。
一种用法是用在子类的构造函数中,super()代表着父类的构造函数,必须写在子类构造函数this之前。起到的作用与SuperType.call(this, name)起到的作用是一样的。让父类的构造函数语句在子类的上下文环境下运行。
二种是super对象,在普通方法中,指向父类的prototype对象即SuperType.prototype,也就是说只有SuperType.prototype上的方法可以被调用,在子类上调用时父类原型方法中的this会指向当前子类,如果在实例上调用时,方法中的this会指向当前实例。
在静态方法中也就是static定义的方法中使用,此方法中指向父类。在子类调用通过super调用父类的静态方法时,this指向同样会变为当前子类。
用代码说明 还是用《高程》上的改造

class SuperType {
  constructor (name){
    this.name = name
    this.colors = ["red", "blue", "green"]
    this.size = 'big'
  }
  saySize() {
    return this.size
  }
  sayName () {
    return this.name
  }
  static sayColor() {
    return this.colors
  }
}
class SubType extends SuperType {
  constructor(name,age,size) {
    super(name)
    this.size = size || 'medium'
    this.age = age
    super.saySize() //?  small, medium  因为下面new了两个实例所以运行了两次都指向了Subtype.protype子类prototype对象
  }
  sayAge() {
   return this.age
  }
  subSaySize() {
    return super.saySize()
  }
  static subSayColor() {
    return super.sayColor() 
  }
}
SubType.colors = ["red", "blue", "green",'white'] //静态属性暂时只能在外面添加
SuperType.sayColor() //? undefined 因为父类没定义静态属性
SubType.subSayColor() //?["red", "blue", "green",'white'] this指向了子类
let instance1 = new SubType('Nicholas',29, 'small')
let instance2 = new SubType('Greg',27)
instance1.subSaySize() //? small 指向子类实例本身
instance2.subSaySize() //? medium 指向子类实例本身

总结 super总是会绑定到当前方法在_Prototype_链中的位置的更高一层,
其实就是在哪调用就指向谁,相当于自动call(this),再就是自动识别是否是静态方法。

Typescript中的类与接口

ts是js的超集,添加了可选的静态类型和基于类的面向对象编程。
ts也是转译成js后运行的,但和es6语法糖不同,当在ts文件中写ts时,语法规则是按照ts来的,譬如加了private属性在转译成js后失效,但是在写ts时是限制外部访问性了的。如果访问会报错。
同样的ts中增加了继承接口抽象类等新概念,虽然可以转译成js再分析在js中会如何,但是这样是没有意义的。因为在js中类只是语法糖是原型的伪装,如果不清楚具体原型链接,那就会写出不能如期运行的代码,但是ts中可以完全假装存在ts定义的类,因为写错了语法提示器会提示错误的。所以这节就不会画原型图并且分析属性的归属,这节按照完全Typescript中定义的类的想法去理解。

基本写法

和es6的class对比,基本一致,但是要先定义name,不然会提示类型‘Person’上不存在属性name

class Person {
    name: String
    constructor(name: String) {
        this.name = name
    }
    sayName() {
        return this.name
    }
}
let person1 = new Person('Mike')
person1.sayName() //?

测试一下发现依然和es6差不多依然是存在充当构造函数的函数,与函数的prototype属性,下面就不再测了。


TS

继承

class SuperType {
    name: string
    colors: Array //此处写出array会报错,其他都能小写这个不行。不知原因是什么
    constructor(name: string) {
        this.name = name
        this.colors = ["red", "blue", "green"]
    }
    sayName () {
        return this.name
    }

}
class SubType extends SuperType {
    age: number
    constructor(name: string,age: number) {
        super(name)
        this.age = age
    }
    sayAge() {
        return this.age
    }
}
let instance1 : SubType
instance1 = new SubType('Nicholas', 28)
instance1.sayAge() //?
instance1.sayName() //?

基本上一样,不赘述。

一些修饰符与特性

  • public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
  • readonly 修饰的属性或方法是只读的,只读属性必须在声明时或构造函数里被初始化
  • static 修饰的属性或方法是静态的,静态属性是定义在类本身上的而不是实力上,prototype对象与实例都不能读取
  • 参数属性 参数属性可以方便地让我们在一个地方定义并初始化一个成员,少了一个赋值操作
class SuperType {
    constructor(public name: string) {
    }
    sayName () {
        return this.name
    }
}
===========================================
function SuperType(name) {
    this.name = name
}
SuperType.prototype.sayName = function () {
    return this.name;
 }
  • 存取器:与es6一样在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
  • 抽象类:抽象类是供其他类继承的基类,本身不允许被实例化。抽象类中的抽象方法必须在子类中被实现

接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implements)。TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对对象的形状(Shape)进行描述。作用就是为这些类型命名和为你的代码或第三方代码定义契约。

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

没有调查就没有发言权,js中本来就没有强制类型,这个接口又是用类型约束对象、函数、类,没使用过。抄官网也没意思,不写了,如果以后用了有心得了再返回来写。

总结

首先,js中原来没有类,连类的概念都没有,一开始只能用prototype模仿,在es6中新加入了class增加了类的写法,但是依然只是语法糖,可以看见和原来的写法基本没区别,还是伪装成的类。在Typescript中的类功能比较完备,并且编写时有较好的错误提示。如果非要用类与继承的思路来写代码就用Typescript。

你可能感兴趣的:(JavaScript中的代码复用——this、对象、类(2)(伪类的旅程))