JavaScript中的面向对象(属性操作、构造函数、原型)

165 阅读9分钟

一、JavaScript的面向对象

  • JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:
    • JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,由key和value组成;
    • key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;
    • 如果值是一个函数,那么我们可以称之为是对象的方法;

1.1. 创建对象的方式

  • 1)使用构造函数创建一个空对象
    var obj = new Object();
    obj.name = 'yzh';
    obj.age = '18';
    obj.height = '1.80';
    obj.eating = function() {
      console.log('吃东西~')
    };
    
  • 2)字面量的形式创建对象
    var obj = {
      name: 'yzh',
      age: '18',
      running: function() {
        console.log('在跑步!')
      }
    }
    

二、对属性操作的控制

  • 前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:
    • 但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for-in遍历的时候被遍历出来呢?
  • 想要对一个属性进行比较精准的操作控制,可以使用属性描述符;
    • 通过属性描述符可以精准的添加或修改对象的属性;
    • 属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改;

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

语法:Object.defineProperty(obj, prop, descriptor);

参数:1)obj要定义属性的对象;2)prop要定义或修改的属性的名称;3)descriptor要定义或修改的属性描述符;

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

  • 属性描述符的类型有两种:
    • 数据属性;
    • 存取属性;

2.1. 数据属性描述符

  • 数据属性描述符四个特性:
    • configurable可配置的,当且仅当该属性的 configurable 键值为 true时,可以通过delete删除属性,编辑属性。默认为false
    • enumerable可枚举,当且仅当该属性的 enumerable 键值为 true 时,可以通过for-in、object.keys()返回该属性。默认为false
    • value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为undefined
    • writable可写的,当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。 默认为 false
    // 1.对象中原来的属性三个特性都是true:
    var info = {
      address: '深圳市',
      hobby: '羽毛球'
    }
    console.log(info); //{ address: '深圳市', hobby: '羽毛球' }
    delete info.address;
    console.log(info); //{ hobby: '羽毛球' }
    
    for (const key in info) {
      console.log(key); //hobby
    }
    console.log(Object.keys(info)); //[ 'hobby' ]
    
    info.hobby = '桀桀桀';
    console.log(info); //{ hobby: '桀桀桀' }
    
    // 2.自己定义属性时三个特性都是默认值(需要手动配置):
    var obj = {
      name: "yzh",
      age: 18
    };
    
    Object.defineProperty(obj, "height", {
      enumerable: true,
      writable: false,
      value: "1.88"
    });
    obj.height = "1.80";
    delete obj.height;
    console.log(obj); //{ name: 'yzh', age: 18, height: '1.88' }
    
    Object.defineProperty(obj, "height", {
      configurable: true,
      enumerable: true,
      writable: true,
      value: "1.88"
    });
    obj.height = "1.80";
    console.log(obj); //{ name: 'yzh', age: 18, height: '1.80' }
    delete obj.height;
    console.log(obj); //{ name: 'yzh', age: 18 }
    

2.2. 存取属性描述符

  • configurable可配置的,同数据属性描述符一样;
  • enumerable可枚举,同数据属性描述符一样;
  • get 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为undefined
  • set属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为undefined
    var obj = {
      name: "yzh",
      age: 18,
      _: "哈哈"
    };
    
    /**
     * 1.隐藏某一个私有属性(js里面是没有严格意义的私有属性),不希望直接被外界使用和赋值;
     * 2.如果我们希望截获一个属性它访问和设置值得过程时,也会使用存储属性描述符;
     * 
     * 不使用Writable、value,同样他们相对的是不能共存的
     */
    Object.defineProperty(obj, "hide", {
      configurable: true,
      enumerable: true,
      get: function() {
        return this._;
      },
    
      set: function(value) {
        this._ = value;
      }
    });
    
    console.log(obj.hide); //哈哈
    obj.hide = "嘿嘿";
    console.log(obj.hide); //嘿嘿
    
    console.log(obj); //{ name: 'yzh', age: 18, _: '嘿嘿', hide: [Getter/Setter] }
    

2.3. 可拥有的键值

object.png

如果一个描述符不具有 valuewritableget 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常;

2.3. 同时定义多个属性

  • Object.defineProperties()  方法直接在一个对象上定义 多个 新的属性或修改现有属性,并返回该对象;
    var obj = {
      _age: 18,
      _eating: function() {}
    };
    
    Object.defineProperties(obj, {
      name: {
        configurable: true,
        enumerable: true,
        writable: true,
        value: "yzh"
      },
    
      age: {
        //enumerable: true,
        get: function() {
          return this._age;
        },
        set: function(value) {
          this._age = value;
        }
      }
    });
    
    obj._age = 20;
    console.log(obj); //{ _age: 20, _eating: [Function: _eating], name: 'yzh' }
    

🍚对象方法补充:

// 获取某一个特性属性的属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, "name"));
console.log(Object.getOwnPropertyDescriptor(obj, "age"));

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

🈲对对象限制:

var obj = {
  name: 'yzh',
  age: 18
};

/* 
* 1.禁止对象继续添加新的属性: preventExtensions(阻止扩展)
* 给一个对象添加新的属性会失败(在严格模式下会报错);
*/
Object.preventExtensions(obj);

obj.height = 1.88;
obj.address = "深圳市";
console.log(obj); //{ name: 'yzh', age: 18 }

/*
* 2.密封对象,不允许配置和删除属性:seal(密封、封上、关闭)
* 实际是调用 preventExtensions
* 并且将现有属性的 configurable:false
*/
// for (var key in obj) {
//   Object.defineProperty(obj, key, {
//     configurable: false,
//     enumerable: true,
//     writable: true,
//     value: obj[key]
//   })
// }
Object.seal(obj);

delete obj.name;
console.log(obj); //{ name: 'yzh', age: 18 }

/*
* 3.冻结对象,不允许修改现有属性:freeze(冻结、冰冻)
* 实际上是调用 seal
* 并且将现有属性的 writable: false
*/
Object.freeze(obj);

obj.name = "hzy";
console.log(obj); //{ name: 'yzh', age: 18 }

三、创建对象

3.1. 创建多个对象的方案

  • 如果我们现在希望创建一系列的对象:包括ace、sabot、luffy等等,他们的信息各不相同;
  • 前面我们已经试过了两种方式:
    • new Object方式;
    • 字面量创建的方式;
  • 这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;
    var obj = {
      name: 'ace',
      age: 22,
      height: '1.88'
    }
    var obj3 = {
      name: 'sabot',
      age: 20,
      height: '1.85'
    }
    var obj4 = {
      name: 'luffy',
      age: 18,
      height: 1.80
    }
    

3.2. 创建多个对象的方案 - 工厂模式

  • 工厂模式其实是一种常见的设计模式;
  • 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;
    // 工厂模式: 工厂函数
    function createPerson(name, age, height, address) {
      var p = {};
      p.name = name;
      p.age = age;
      p.height = height;
      p.address = address;
    
      p.eating = function() {
        console.log(this.name + "在吃东西~");
      };
    
      p.running = function() {
        console.log(this.name + "在跑步~");
      };
    
      return p;
    }
    
    var p1 = createPerson("luffy", 18, 1.78, "广州市");
    var p2 = createPerson("sabo", 20, 1.88, "上海市");
    var p3 = createPerson("ace", 22, 1.98, "北京市");
    
    console.log(p1, p2, p3)
    

优点:通过工厂模式,我们可以快速创建大量相似对象,没有重复代码;

缺点:工厂模式创建的对象属于Object,无法区分对象类型,这也是工厂模式没有广泛使用的原因;

四、认识构造函数

  • 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;
  • 构造函数也是一个普通的函数,函数被使用new操作符调用了,那么这个函数就是构造函数;

4.1. new操作符调用的作用

  • 如果一个函数被使用new操作符调用了,那么它会执行如下操作:
    • 1)在内存中创建一个新的对象(空对象)
    • 2)这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;
    • 3)构造函数内部的this,会指向创建出来的新对象
    • 4)执行函数的内部代码(函数体代码);
    • 5)如果构造函数没有返回非空对象,则返回创建出来的新对象
      function Foo() {
      
      };
      
      var fn = new Foo();
      console.log(fn); //Foo {}
      

4.2. 创建多个对象的方案 - 构造函数

  • 这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性);
    function Person(name, age, height, address) {
      this.name = name;
      this.age = age;
      this.height = height;
      this.address = address;
    
      this.eating = function() {
        console.log(this.name + "在吃东西");
      };
    
      this.running = function() {
        console.log(this.name + "在跑步");
      }
    };
    
    var p1 = new Person("ace", 22, 1.98, "深圳市");
    console.log(p1);
    
    var p2 = new Person("luffy", 20, 1.78, "上海市");
    console.log(p2);
    

构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例

同时会创建出重复的函数;

五、认识原型

5.1. 对象的原型理解

  • JS每个对象中都有一个 [[prototype]],这个特殊的对象属性可以称之为对象的原型(隐式原型);
  • 这个特殊的对象可以指向另外一个对象;
    var obj = { name: "yzh" } 
    var info = {} 
    console.log(obj);
    console.log(info);
    
    obj_prototype.png
  • 获取这个对象的方式:
    • 1)通过对象的 __proto__ 属性可以获取到;
    • 2)通过 Object.getPrototypeOf 方法可以获取到
      console.log('obj---', obj.__proto__)
      console.log('info---', info.__proto__) 
      
      // ES5之后提供的Object.getPrototypeOf
      console.log(Object.getPrototypeOf(obj))
      
  • 这个对象的作用在于:当我们从一个对象中获取某一个属性时, 它会触发 [[get]] 操作;
    • 1)在当前对象中去查找对应的属性, 如果找到就直接使用;
    • 2)如果没有找到,那么会访问对象[[prototype]]内置属性指向的对象上的属性(沿着它的原型去查找);
      obj.__proto__.age = 18
      obj.__proto__.height = 1.88
      
      console.log(obj.age) //18
      

5.2. 函数的原型理解

  • 函数也是一个对象
  • 函数作为对象来说, 它也是有[[prototype]] 隐式原型(因为它是一个函数,才有了这个特殊的属性);
  • 函数它因为是一个函数, 所以它还会多出来一个显示原型属性: prototype;
function foo() {
}

console.log(foo.__proto__)

console.log(foo.prototype)

var f1 = new foo()
var f2 = new foo()
console.log('f1: ', f1);
console.log('f2: ', f2);
console.log(f1.__proto__ === foo.prototype) // true
console.log(f2.__proto__ === foo.prototype) // true

5.3. 再看new操作符

  • 图.png: image.png

  • 那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype;

    function Person() {};
    
    var p1 = new Person();
    var p2 = new Person();
    /***
     * 上面的操作会进行如下:
     * p = {}
     * p.__proto__ = Person.prototype
     */
    
    //所以
    console.log(p1.__proto__ === Person.prototype); //true
    console.log(p2.__proto__ === Person.prototype); //true
    

5.4. 函数原型上的属性

  • constructor属性:原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
    function foo() {
    
    }
    
    const f1 = new foo();
    
    console.log(foo.prototype.constructor) // [Function: foo]
    console.log(foo.prototype.constructor.name) //foo
    console.log(p1.__proto__.constructor) // [Function: foo]
    console.log(p1.__proto__.constructor.name) //foo
    
  • 我们也可以添加自己的属性:
    foo.prototype.name = "yzh"
    foo.prototype.age = 18
    foo.prototype.height = 1.88
    foo.prototype.eating = function() {
    
    }
    
    var f1 = new foo()
    console.log(f1.name, f1.age)
    
  • 如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象:
    • 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;
    • 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是foo构造函数了;
      function foo() {
      
      }
      
      foo.prototype = {
        name: "yzh",
        age: 18,
        height: 1.88
      }
      
      var f1 = new foo()
      
      console.log(foo.prototype.constructor) //[Function: Object]
      
      console.log(f1.name, f1.age, f1.height) //yzh 18 1.88
      
  • 如果希望constructor指向foo,那么可以手动添加:
    foo.prototype = {
      constructor: foo,
      name: "yzh",
      age: 18,
      height: 1.88
    }
    
  • 真实开发中我们可以通过Object.defineProperty方式添加constructor:
    Object.defineProperty(foo.prototype, "constructor", {
      enumerable: false,
      configurable: true,
      writable: true,
      value: foo
    })
    

六、创建对象 – 构造函数和原型组合

  • 将函数放到Person.prototype的对象上,让所有的对象去共享这些函数;
    function Person(name, age, height, address) {
      this.name = name;
      this.age = age;
      this.height = height;
      this.address = address;
    };
    
    Person.prototype.eating = function() {
      console.log(this.name + "在吃东西");
    };
    
    Person.prototype.runing = function() {
      console.log(this.name + "在跑步");
    };
    
    var p1 = new Person("luffy", 18, 1.88, "深圳市");
    var p2 = new Person("ACE", 22, 1.98, "上海市");
    
    p1.eating(); //luffy在吃东西
    p1.runing(); //luffy在跑步
    
    p2.eating(); //ACE在吃东西
    p2.runing(); //ACE在跑步