美文网首页前端学习
前端基础整理 | Javascript基础 (二)对象与继承

前端基础整理 | Javascript基础 (二)对象与继承

作者: 格致匠心 | 来源:发表于2019-06-10 01:44 被阅读3次

    个人向,对JS知识进行了查漏补缺,主要来源于《JS高级程序设计》和网上博客,本文内容主要包括以下:

    1. 对象
    2. 创建对象
    3. 继承

    一、对象

    特性(attribute),描述了属性(property)的各种特征。内部使用,不能直接访问,两对方括号括起来。

    1. 数据属性:

    • 定义:包含一个数据值的位置,在这个位置可以对数据值进行读写。
    • 创建方法:定义对象的时候的键值对就是数据属性啦。
    • 特性:
      • [[Configurable]] :表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
      • [[Enumerable]] :表示能否通过for-in循环返回属性,默认为 true
      • [[Writable]] :表示能否修改属性的值,默认为 true
      • [[Value]] :包含该属性的数据值。默认为 undefined
    • 设置属性方法:
      Object.defineProperty(person, 'name', {
          configurable: true, //可以被删除,可以修改特性,可以修改为访问器属性
          writable: false, //不可以写入其他值
          enumerable: true, //可以for-in遍历
          value: 'tony' //值是tony
      })
      Object.getOwnPropertyDescriptor(person,'name').configurable // 查看特性
      

    ❗在使用defineProperty创建时候,未定义configurable / writable / enumerable都是默认false

    2. 访问器属性

    • 创建方法:不能直接定义,只能通过Object.defineProperty()方法来定义。
    • 特性:
      • [[Configurable]] :表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
      • [[Enumerable]] :表示能否通过for-in循环返回属性,默认为true
      • [[Get]] :读取属性调用的函数,默认undefined
      • [[Set]] :写入属性调用的函数,默认undefined
    • 设置属性方法:
      var person = {
        _age:10,
        isAdult: false
      }
      Object.defineProperty(person, 'age', {
        get: function () { // 只指定getter那默认不能write只能read
          return this._age
        },
        set: function (val) { //只指定setter那默认不能read只能write
          this._age = val
          if (val > 18)
            this.isAldult = true
          else
            this.isAldult = false
        }  
      })
      
    • 定义多个属性
      Object.defineProperties(book, {
        _name: {
          writable: true,
          configurable: true,
          value: 'tony'
        },
        name: {
          get: function(){},
          set: function(){}
        }
      })
      

    二、创建对象

    1. 工厂模式:

    • 优点:解决了创建多个相似对象;
    • 缺点:但问题是无法识别对象的类型。
    function createPerson(name, age) {
      let o = new Object()
      o.name = name
      o.age = age
      o.sayName = function() { alert(o.name); }
      return o;
    }
    

    2. 构造函数模式

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

    实际上,任何函数都可以是构造函数,只要配上new。而构造函数没有用new来调用,也是一个普通函数。

    • 那么new做了什么呢?
    1. 创建一个新对象
    2. 把构造函数的作用域赋给新对象(this指向新对象)
    3. 执行构造函数代码(给新对象添加属性)
    4. 返回新对象

    手动模拟一下new的工作:

    function newPerson(name, age) {
      let o = new Object()
      Person.call(o, name, age)
      return o
    }
    tony = newPerson('tony', 10)
    
    • 缺点
      构造函数也存在问题:每个方法都在实例上重新创建一遍。可以用这段代码证明:console.log(person1.sayName === person2.sayName) // false
      当然,我们可以在外部声明函数,然后在构造函数中引用该函数。但是这么做会在全局作用域定义很多函数,封装性大大降低。而原型模式能很好地解决这一点,因为它可以让所有对象实例共享它所包含的属性和方法。

    3. 原型模式

    • 原型(prototype)是什么?
      prototype是一个指针,指向函数原型对象。prototype是一个函数的属性。
      (理解原型的前提是要知道,函数本身也是一个对象,prototype是它的属性之一,指向一个叫原型对象的东西)

    • 一张经典的图片

      一张经典的图片
    • prototype 与 __proto__
      当创建函数的时候,函数的原型对象自动获得一个constructor属性,该属性指向这个函数。
      当创建实例的时候,实例内部也包括一个指针[[Prototype]],指向构造函数的原型对象。这个东西在chrome之类的浏览器实现为__proto__指针。在ES5中标准的拿实例对象原型的方法是Object.getPrototypeOf()

      三种原型对象的取法
    • 原型对象挂载方法和属性
      我们可以向原型对象上挂属性和方法,这样每个使用这个原型的实例都能读到。并且,我们解决了构造函数方法每次实例化都创建的问题。

      image.png
    • 判断属性方法

    person.hasOwnProperty('name') //true因为这是来自实例的属性。
    person.hasOwnProperty('age') //false因为这是来自原型继承来的属性。
    'age' in person //true 'in'操作符在对象能访问该属性时返回true
    
    • 获取属性方法
    Object.keys(Person.prototype) //获取[[Enumerable]]为true的可枚举实例属性
    Object.getOwnPropertyNames() //获取所有的实例属性
    
    Reflect.ownKeys(person) // 获取所有的实例属性以及symbol
    Object.getOwnPropertyNames(person).concat(Object.getOwnPropertySymbols(person)) // 就是上面代码的实际返回
    
    • 缺点
      原型对象的缺点是:省略了为构造函数传递初始化参数的环节,导致默认情况下取得相同属性

    4. 原型+构造函数模式

    目前ECMAScript中最广泛的模式,就是构造函数模式用来定义实例属性,原型模式用来定义方法和共享的属性。

    5. 动态原型模式

    其实就是在构造函数内弄了一个判断语句,当不存在一个方法的时候,将方法挂在原型上。

    function Person(name) {
      this.name = name
      if ( typeof this.sayName!='function') {
        Person.prototype.sayName = function() { alert(this.name)}
      }
    }
    

    6.寄生构造模式

    代码和工厂模式一样,就是用new来创建,暂时不做分析

    7.稳妥构造模式

    有种闭包的感觉,不用this进行构造,没有公共属性,用在需要特殊的安全执行环境。

    三、 继承

    1. 原型链

    MDN文档的描述再结合上面的"一张经典的图片"食用更佳:
    原型链的顶端是Object,再往上就是null了,null没有原型。

    JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

    2. 借用构造函数继承

    • 核心:
      子类调用父类的构造函数从而实现属性的继承
    • 优点:
      1.可以向父类传递参数
      2.父类的引用属性不会被子类实例共享
    • 缺点:
      1.父类方法不能复用
      2.对子类实例使用 instanceof 只会识别到子类
    function Person(name){
      this.name = name
    }
    function Student(name){
      Person.call(this, name)
    }
    

    3. 原型链继承

    • 核心:子类把prototype指向父类的一个实例对象
    • 优点:1.父类方法可复用;2. instanceof 可以识别到父类子类
    • 缺点:1.子类构建实例时不能传参;2. 父类的引用属性会被所有子类实例共享
    function Person(name){
      this.name = name
    }
    person = new Person('tony')
    function Student(){}
    Student.prototype = person 
    Student.prototype.constructor = Student
    

    4. 原型链+构造函数 组合继承

    • 核心:子类调用父类构造函数来实现属性继承,prototype指向父类的实例对象。
    • 优点:构造函数和原型链互补,即父类方法可复用 & 父类引用类型属性不会被共享。
    • 缺点:构造函数调用了两次,造成性能浪费,并且可能会覆盖子类同名属性。
    function Person(name){
      this.name = name
    }
    Person.prototype.sayHi = function(){alert('hello')}
    function Student(name){
      Person.call(this,name)
    }
    Student.prototype = new Person()
    Student.prototype.constructor = Student
    

    5. 原型式继承

    • 核心:创建临时构造函数,把传入对象作为构造函数的原型,返回临时类型的新实例。
    function object(o){
      function F(){}
      F.prototype = o
      return new F()
    }
    let person = {
        name: 'tony'
    };
    let anotherPerson = object(person)
    

    ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。

    所以上述可以简化为 let anotherPerson = Object.create(person)

    6. 寄生继承

    只是一种思路而已,没什么优点,通过给使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

    function createAnother(original){ 
        var clone=object(original)
        clone.sayHi = function(){
            alert("hi");
        };
        return clone
    }
    var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    };
    var anotherPerson = createAnother(person)
    anotherPerson.sayHi()
    

    7. 寄生组合式继承

    目前最完美的继承方法,只需要在继承函数中调用构造函数再使用下面的继承就行了。

    function inheritPrototype(subType, superType){
        var prototype = Object.create(superType.prototype); // 创建了父类原型的浅复制
        prototype.constructor = subType;             // 修正原型的构造函数
        subType.prototype = prototype;               // 将子类的原型替换为这个原型
    }
    

    为了方便理解,这里有两个类似的继承函数。第一个是使用类似原型构造的F函数,第二个是直观的展示了继承在Chrome等具有__proto__指针中的形式。

    function F_inherits(Child, Parent) {
      var F = function() {}
      F.prototype = Parent.prototype
      Child.prototype = new F()
      Child.prototype.constructor = Child
    }
    function myInherits(Child, Parent) {
      Child.prototype = { constructor: Child, __proto__: Parent.prototype }
    }
    

    8. class 继承(ES6)

    ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

    • 语法:
    class A {}
    class B extends A {
      constructor() {
        super();
      }
    }
    
    • 实现原理:
    class A {}
    class B {}
    Object.setPrototypeOf = function (obj, proto) {
      obj.__proto__ = proto;
      return obj;
    }
    // B的实例继承A的实例
    Object.setPrototypeOf(B.prototype, A.prototype);
    // B 继承 A 的静态属性
    Object.setPrototypeOf(B, A);
    

    ES6继承与ES5继承的异同:

    • 相同点:本质上ES6继承是ES5继承的语法糖
    • 不同点:
      1. ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
      2. ES6子类实例的构建,基于父类实例,ES5中不是。

    四、一些自己实现的函数

    帮助大家更好地理解:对象、继承。

    /**
     * call实现
     */
    Function.prototype._call = function(ctx, ...args) {
      ctx = ctx || window
      ctx.func = this
      let result = ctx.func(...args)
      delete ctx.func
      return result
    }
    
    /**
     * apply实现
     */
    Function.prototype._apply = function(ctx, args) {
      ctx = ctx || window
      ctx.func = this
      let result = ctx.func(...args)
      delete ctx.func
      return result
    }
    
    /**
     * bind实现
     */
    Function.prototype._bind = function(target) {
      target = target || window
      const that = this
      const args = [...arguments].slice(1)
      let fn = function() {
        return that.apply(
          this instanceof fn ? this : target,
          args.concat(...arguments)
        )
      }
      let F = function() {}
      F.prototype = this.prototype
      fn.prototype = new F() // fn.prototype.__proto__ == this.prototype  true
      return fn
    }
    
    /**
     * instanceof实现
     */
    function _instanceof(a, b) {
      let prototype = b.prototype
      let a = a.__proto__
      while (true) {
        if (a === null || a === undefined) {
          return false
        } else if (a === prototype) {
          return true
        } else {
          a = a.__proto__
        }
      }
    }
    

    相关文章

      网友评论

        本文标题:前端基础整理 | Javascript基础 (二)对象与继承

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