JavaScript 之面对对象

111 阅读7分钟

在 JavaScript 中,大多数事物都是对象。

理解对象

如下例子,定义了一个对象person,这个对象拥有属性 namesayName() 方法。我们可以通过访问属性和方法。

var person = {
  name:"Jack",
  sayName:function(){
    console.log(this.name);
  }
}

console.log(person.name);  // Jack
person.sayName()           // Jack

设置对象成员

我们看到了如何访问对象的成员,也可以设置对象成员的值,通过声明你要设置的成员,如下:

person.name = "Alice"
person.age = 18 // 等同于 person['age'] = 18
person.farewell = function() { console.log("Bye!") }

console.log(person.name);       // Alice
console.log(person.age);        // 18
person.farewell()               // Bye!

修改了原来的 name ,新增了 age 属性,新增了 farewell() 方法

属性类型

javascript 对象属性的类型分为两种:数据属性和访问器属性

数据属性

数据属性将键与值相关联。它可以通过以下属性来描述:

  • value 存储这个属性的值
  • writable 表示能否修改属性的值,默认为 true
  • enumerable 表示是否可以通过 for...in 循环来枚举属性,默认为 true
  • configurable 表示能否通过 delete 删除属性,能否把属性修改为访问器属性,能否修改属性的特性,默认为 true
var person = {
  name:"Jack",
}

这里定义的对象属性 name 值为 jack,表示[[value]]特性将被设置为 jack。任何对这个值的修改都将反应在这个位置

Object.defineProperty()

要修改属性内部的特性,必须使用ECMAScript5Object.defineProperty()

语法

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty() 接受三个参数:

  • obj 属性所在的对象
  • prop 属性的名字
  • descriptor 定义或修改的属性描述符
var person = {}
Object.defineProperty(person, 'name', {
  writable: false,
  value: 'jack',
  configurable: true,
  enumerable: true,
})

console.log(person.name) // jack
person.name = 'alice'
console.log(person.name) // jack

这里我们给一个空对象 person,设置了 name 属性,它的值为 jack 是只读的。

访问器属性

访问器属性不包含数据值:

  • get 读取属性时调用,默认 undefined
  • set 写入属性时调用,默认 undefined
  • enumerable 表示是否可以通过 for...in 循环来枚举属性,默认为 true
  • configurable 表示能否通过 delete 删除属性,能否把属性修改为访问器属性,能否修改属性的特性,默认为 true
var book = {
  _year:2022,
  edition:1
}

Object.defineProperty(book,'year',{
  get:function(){
    return this._year
  },
  set:function(newValue){
    if(newValue > this._year){
      this._year = newValue
      this.edition += newValue - 2022
    }
  }
})

book.year = 2023
console.log(book.edition); // 2

以上代码定义了一个 book 对象,并定义了两个属性:_year 和 edition。访问器属性包含一个 getter 函数和一个 setter 函数。getter 函数返回 _year 的值,setter 函数根据 year值的变化修改 _year和edition

定义多个属性

Object.defineProperties()可以一次性定义多个属性,上面的代码可以这样写:

var book = {}
Object.defineProperties(book,{
  _year:{
    writable:true,
    value:2004
  },
  edition:{
    writable:true,
    value:1
  },
  year:{
    get:function(){
      return this._year
    },
    set:function(newValue){
      if(newValue > this._year){
        this._year = newValue
        this.edition += newValue - 2022
      }
    }
  }
})

读取属性的特性

Object.getOwnPropertyDescriptor()方法可以获取属性的描述符。

var book = {}
Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004,
  },
  edition: {
    writable: true,
    value: 1,
  },
  year: {
    get: function () {
      return this._year
    },
    set: function (newValue) {
      if (newValue > this._year) {
        this._year = newValue
        this.edition += newValue - 2022
      }
    },
  },
})

var d1 = Object.getOwnPropertyDescriptor(book, '_year')
console.log(d1.value) // 2004
console.log(d1.writable) //true
console.log(d1.enumerable) //false
console.log(d1.configurable) //false
console.log(d1.get);  // undefined

var d2 = Object.getOwnPropertyDescriptor(book, 'year')
console.log(d2.value) // undefined
console.log(d2.writable) //undefined
console.log(d2.enumerable) //false
console.log(d2.configurable) //false
console.log(d2.get) // function

对于数据属性自己的属性都可以访问到,未设置的取默认值。数据属性访问 getter 函数就是undefined。访问器属性反之亦然。

创建对象

Object() 构造函数

Object 构造函数将给定的值包装为一个新对象。

  • 如果给定的值是 null或undefined, 它会创建并返回一个空对象。
  • 否则,它将返回一个和给定的值相对应的类型的对象。
  • 如果给定值是一个已经存在的对象,则会返回这个已经存在的值(相同地址)
var person = {
  name:'jack',
  age:20
}
var o1 = new Object()
var o2 = new Object(null)
var o3 = new Object(undefined)
var o4 = new Object({
  name:'jack'
})
var o5 = new Object(person)

console.log(o1); // {}
console.log(o2); // {}
console.log(o3); // {}
console.log(o4.name); // jack
console.log(o5 === person); // true

Object.create()

Object.create()  方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)

const person = {
  name:'jack',
  sayName:function(){
    console.log(this.name);
  }
};

const me = Object.create(person);

me.name = 'Matthew';

me.sayName();  // Matthew

console.log(me);

image.png

对象字面量

var obj = {} // 等同于 var obj = new Object()
var person = {
  name:"Jack",
  age:20,
  sayName:function(){
    console.log(this.name);
  }
}

工厂模式

通过函数封装以特定接口创建对象。工厂模式的缺点在于无法识别对象的类型

function createPerson(name,age){
  var obj = new Object()
  obj.name = name
  obj.age = age
  obj.sayName = function(){
    console.log(this.name);
  }
  return obj
}

var p1 = createPerson("jack",20)
var p2 = createPerson("alice",18)

构造函数模式

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

var p1 = new Person("jack",20)
var p2 = new Person("alice",18)

console.log(p1);
console.log(p2);
console.log(p1.sayName === p2.sayName); // false

构造函数的缺点是虽然它们两个实例都有sayName()方法,但两个方法不是同一个 Function 实例

原型模式

函数的原型对象共享属性和方法给所有实例。

function Person(){}

Person.prototype.name = 'Jack';
Person.prototype.age = 20;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName();   // Jack

var person2 = new Person();
person2.sayName();   // Jack

console.log(person1.sayName == person2.sayName);  // true

这里将所有的属性和方法定义在prototype属性上。所有的实例都是访问的同一个属性和实例。 了解更多的原型可以前往 JavaScript 之原型与原型链 - 掘金 (juejin.cn)

通过字面量形式重写原型:

function Person(){}

Person.prototype = {
    constructor: Person, // 重写原型一定要将 constructor 属性赋值为原构造函数,否则原型丢失
    name : 'Jack',
    age : 20,
    hobby: ['read','sports'],
    sayName : function () {
      console.log(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person2.hobby.push('travel')

console.log(person1.hobby);    // ['read', 'sports', 'travel']
console.log(person2.hobby);    // ['read', 'sports', 'travel']
console.log(person1.sayName === person2.sayName);  // true

这里因为共享了引用属性 hobby,修改了一个实例属性值,将影响其它的地方

组合使用构造函数模式和原型模式

最常见创建自定义了下的方式就是组合使用构造函数模式和原型模式。构造函数模式用来定义实例属性,原型模式用来定义方法和共享属性。这样每个实例都有自己的实例属性,同时又共享方法。

function Person(name, age) {
  this.name = name
  this.age = age
  this.hobby = ['read', 'sports']
}
Person.prototype = {
  constructor: Person,
  sayName: function () {
    console.log(this.name)
  },
}
var person1 = new Person('Jack', 20)
var person2 = new Person('Alice', 18)

person2.hobby.push('travel')

console.log(person1.hobby) // ['read', 'sports']
console.log(person2.hobby) // ['read', 'sports', 'travel']
console.log(person1.hobby === person2.hobby) //false
console.log(person1.sayName === person2.sayName) // true

此时修改引用属性,将不会影响其它实例属性值

动态原型模式

把所有的信息都封装在了构造函数中,通过检查原型上要定义的方法是否有效来决定是否需要初始化原型

function Person(name, age) {
  this.name = name
  this.age = age
  if (typeof this.sayName != 'function') {
    Person.prototype.sayName = function () {
      console.log(this.name)
    }
  }
}
var person1 = new Person('Jack', 29)
var person2 = new Person('Alice', 27)

person1.sayName()
person2.sayName()

这里只有sayName()方法不存在的情况下,才会将它添加在原型上。只会在第一次调用构造函数时执行。

备注:使用动态原型模式时,不能使用对象字面量重写原型,在已经创建实例的情况下重写原型,会切断现有实例与新原型之间的联系

寄生构造函数模式

这种模式算是结合了构造函数模式和工厂模式的一种模式。需要通过new的方式调用,然后手动返回新对象覆盖了构造函数的默认值。函数内部仅仅只是封装了创建对象的代码。 寄生模式用于创建一些特殊的原生对象,避免污染到其他原生对象

function Person(name,age){
  var o = new Object()
  o.name = name
  o.age = age
  o.sayName = function(){
    console.log(this.name);
  }
  return o
}

var person = new Person('jack',20)
person.sayName() // jack

返回的对象与构造函数或者与构造函数的原型属性之间没有关系。构造函数返回的对象和外面创建的对象一样

function SpecialArray() {
  var values = new Array()

  values.push.apply(values, arguments)

  values.toPipedString = function () {
    return this.join('|')
  }

  return values
}

var colors = new SpecialArray('red', 'blue', 'green')
console.log(colors.toPipedString()) // red|blue|green

values.push.apply(values,arguments)就是用构造函数接收所有参数,相当于:

for (let i = 0; i < arguments.length; i++) {
  values.push(arguments[i])
}

稳妥构造函数模式

通过稳妥构造函数模式创建的对象,没有公共属性,方法也不引用this对象。

特点:

  1. 新创建的对象实例方法不用this
  2. 不使用new操作符调用构造函数
function Person(name, age) {
  var o = new Object()
  o.sayName = function () {
    console.log(name)
  }
  return o
}
var p = Person('jack', 20)
p.sayName() // jack
console.log(p.name) // undefined

这里的实例就只能通过 sayName() 方法来访问 name 属性