美文网首页
JavaScript中的代码复用——this、对象、类(2)(伪

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

作者: 李向_c52d | 来源:发表于2018-10-13 18:44 被阅读0次

    前言

    一共有三篇,这一篇写在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<string> //此处写出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)(伪

          本文链接:https://www.haomeiwen.com/subject/emlqaftx.html