JavaScript 的面向对象(OO)

在面向对象编程的语言中,都有类的概念,可以基于这个类创建无数个拥有相同属性和方法的对象。在js中,是没有类的概念的,所以会略有所不同。

对象: {} 这就是一个对象,对,没错,就是这么简单。我们可以将对象想象成一个散列表,无非就是一些键值对,值可以是数据或函数。每一个对象都是基于引用类型创建的,可以是原生(基本、引用)类型,也可以是自定义类型。

基本类型(按值访问): Number, String, Undefined, Unll, Boolean, Symbol.

引用类型(按引用访问): Object, Function, Array, Date, RegExp...

  1. 在最早的时候,我们都是这样创建对象的

最早

const obj = new Object()
obj.name = 'len'
obj.age = 23
obj.sayName = function() {
    console.log(this.name)
}

obj.sayName() // len
复制代码

A few years later...

字面量

const obj = {
    name: 'len',
    age: 23,
    sayName: function() {
        console.log(this.name)
    }
}
复制代码

这样创建的两个对象是一样的,都有相同的属性和方法。这些属性在创建的时候,都会带有一些特征值。

属性分两种:1. 数据属性 2. 访问器属性

数据属性

configuration: 是否通过delete操作删除从而重新定义。 默认 true
enumeration: 是否能通过for...in 循环返回属性。 默认 true
writable:  能否修改属性的值。 默认 true
value:  属性的值。 默认 undefined
复制代码

我们可以这样设置一个数据属性的属性特征值

let obj = {}
Object.defineProperty(obj, 'name', {
    configuration: true,
    enumeration: false,
    writable: false,
    value: 'len'
})

obj.name // len
obj.name = 'lance'
obj.name // len 因为writable为false,赋值会被忽略,在严格模式下会报错
复制代码

tips: 当一个属性的configuration的特征值为设置为false的时候,也就是不能配置的时候,就不能再变回可配置的了,

let obj = {}
Object.defineProperty(obj, 'name', {
    configuration: false,
    value: 'len'
})

obj.name // len
delete obj.name
obj.name // len 因为configuration为false,delete会被忽略,严格模式下会报错

// 这时候我们再这样,除了修改writable以外,其他的都会导致报错
Object.defineProperty(obj, 'name', {
    configuration: true
})

调用的时候,如果不指定这些特征值,默认为false
复制代码

访问器属性

configuration: 是否可配置 默认 true
enumeration: 是否可枚举 默认 true
get: 读取属性的时候调用 默认 undefined
set: 设置属性的时候调用 默认 undefined
复制代码

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。请看:

var obj = {
  _year: 2018,
  count: 1
}

Object.defineProperty(obj, 'year', {
  get: function() {
      console.log('get')
      return this._year
  },
  set: function(newValue) {
      console.log('set')
      if (newValue > 2018) {
        this._year = newValue
        this.count += newValue - 2018
      }
  }
})

console.log(obj.year) // get 2018

obj.year = 2019 // set

console.log(obj.year) // get 2019

console.log(obj.count) // 2
复制代码

_year前面的下划线是一种常用的几号,表示只能通过对象方法访问属性。而访问器属性包含getter、setter, getter返回 _year值, setter设置 _year的值。不一定非要同时指定getter和setter,只指定getter表示属性是只读的。

我们可以用Object.defineProperties()一下定义多个属性

var obj = {}

Object.defineProperties(obj, {
    _year: {
        value: 2018
    },
    count: {
        value: 1
    },
    year: {
        get: functin() {
            return this._year
        },
        set: function(newValue) {
            if (newValue > 2018) {
                this._year = newValue
                this.count += newValue - 2018
            }
           
        }
    }
})
复制代码

注意:数据描述符和存取描述符不能混用,也就是writable或者value和get,set不能混用等...

使用字面量创建对象的缺点: 重复代码太多,无法复用

工厂函数

function person(name, age) {
    var obj = new Object()
    obj.name = name
    obj.age = age
    obj.sayName = function() {
        return this.name
    }
    return obj
}

var p1 = person('len', 23)
p1.name // len
p1.age // 23
p1.sayName() // len
var p2 = person('lance', 23)
p2.name // lance
p2.age // 23
p2.sayName() // lance
复制代码

优点: 解决了字面量重复代码,无法复用的问题

缺点: 不能确定对象的类型。

什么叫不能确定对象的类型。来,我们看下这个

console.log(p1 instanceof person) // false
console.log(p1 instanceof Object) // true
复制代码

所有的实例对象都是Object,自然而言就没法确定了

构造函数

// 构造函数一般首字母大写
function Person(name, age) {
    this.name = name
    this.age = age
    this.sayName = function() {
        return this.name
    }
}

let p1 = new Person('len', 23)
console.log(p1.name) // len
console.log(p1.age) // 23
console.log(p1.sayName()) // len

let p2 = new Person('lance', 23)
console.log(p2.name) // lance
console.log(p2.age) // 23
console.log(p2.sayName()) // lance
复制代码

和工厂函数的不同

  1. 没有显示的创建对象
  2. 没有将属性和方法赋值给this对象
  3. 没有return语句
  4. 生成实例对象的时候使用了new关键字

正是使用了new关键字 ,所以才没有做上述的操作,可想而知,都是new在底层帮我们实现了上述功能

new内部所做的事情

  1. 生成一个新的对象
  2. 将构造函数的作用域赋给新对象(this 就指向了新对象)
  3. 执行构造函数中的代码 (给新对象添加了属性和方法)
  4. 返回新对象

手动实现new

function createObj() {
    // 1. 生成新对象
    let obj = new Object()
    let cons = [].unsift.call(arguments)
    // 3. 执行构造函数中的代码
    obj.__proto__ = cons.prototype
    // 2. 将构造函数的作用域赋给新对象
    const res = cons.apply(obj, arguments)
    return typeof res === 'object' ? res : obj
}
复制代码

构造函数的调用方式

  1. 当做构造函数使用 let p = new Person()
  2. 当做普通函数使用 let p = Person() 这时候如果在浏览器中 this指向的就是window
  3. 在另一个对象的作用域中调用 let o = new Object() / Person.call(o)

优点: 解决了不知道对象类型的问题

console.log(person1.constructor == Person) //true
console.log(person2.constructor == Person) //true

console.log(p1 instanceof Person) // true
console.log(p1 instanceof Object) // true

console.log(p2 instanceof Person) // true
console.log(p2 instanceof Object) // true
复制代码

这样就知道了p1,p2都是Person的实例对象

function Human() {}

let h = new Human()

console.log(h instanceof Human) // true

这样h就是Human的实例了,这样就做到了区分
复制代码

缺点: 每个方法都要在每个实力上重新创建一遍, 请看

// 从逻辑角度讲, 是可以这么定义的

function Person(name, age) {
    this.name = name
    this.age = age
    
    this.sayName = new Function() {
        console.log(this.name)
    }
}
复制代码

每个Person实例都包含一个不同的Fuction实例以显示name属性。 以这种方法创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制还是一样的。因此,不同实力上的同名函数是不相等的, 一下代码是可以证明的。

console.log(p1.sayName === p2.sayName) // true

因此,为了解决这个问题,我们可以使用原型模式

原型模式

function Person() {
    
}

Person.prototype.name = 'len'
Person.prototype.age = 23
Person.prototype.sayName = function() {
    return this.name
}

let p1 = new Person()
console.log(p1.name) // len
console.log(p1.age) // 23
console.log(p1.sayName()) // len

let p2 = new Person()
console.log(p2.name) // len
console.log(p2.age) // 23
console.log(p2.sayName()) // len

console.log(p1.sayName === p2.sayName) // true
复制代码

上面之所以能打印,是因为原型链的关系,实例上没找到,在原型上找到了

这里解释下原型对象,这样有助于我们理解原型继承

原型对象:无论什么时候,我们只要创建了一个新函数,就会根据一组特定的规则为该函数创一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象会获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针Person.prototype.constructor === Person // true,Person是构造函数,Person.prototype是原型对象。创建构造函数之后,默认只会取得constructor属性,其他属性都是从Object继承而来的。那为什么当我们获取p1.name的时候,会从原型上找呢,是因为,在每个实例对象当做,都有一个__proto__属性指向构造函数的原型对象,也就是Person.prototype,所以才能在原型上找到。又因为Person是从Object继承而来,所以,Person.prototype.proto === Object.prototype,Object和Object.prototype之前的关系就像p1和Person之间的关系,所以,这样就形成了原型链。

虽然在所有的实现中,我们都无法访问到__proto__,但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系

console.log(Person.prototype.isProptotypeOf(p1)) // true
console.log(Person.prototype.isProptotypeOf(p2)) // true
复制代码

在es6中增加了一个新方法,叫Object.getPrototypeOf(),该方法返回__proto__的值

console.log(Object.getPrototypeOf(p1) === Person.prototype) // true
console.log(Object.getPrototypeOf(p1).name) // len
复制代码

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

function Person() {}

Person.prototype.name = 'len'

let p1 = new Person()
let p2 = new Person()
p1.name = 'lance'

console.log(p1.name) // lance
console.log(p2.name) // len 还是原型中的值
复制代码

我们可以通过hasOwnProperty()来判断属性是实例的还是原型的

function Person() {}

Person.prototype.name = 'len'

let p1 = new Person()
let p2 = new Person()

console.log(p1.getOwnProperty('name')) // false
p1.name = 'lance'
console.log(p1.getOwnProperty('name')) // true

delete p1.name // delete 可以完全删除实例属性
console.log(p1.getOwnProperty('name')) // false
复制代码

我们还可以用in来判断

function Person() {}

Person.prototype.name = 'len'

let p1 = new Person()

console.log('name' in p1) // true

p1.name = 'lance'

console.log('name' in p1) // true
复制代码

所以in操作符不管是实例属性还是原型上的属性,只要找到了,就返回true

我们还可以用hasOwnProperty(),只在属性存在于实例中的时候才返回true, 所以我们可以结合in使用,来判断,属性到底是原型的属性还是实例的属性

for...in返回的是不管是实例上的还是原型上的,只要是enumeration不为false的所有属性集合。

在es6中,可以使用Object.keys()来获取所有可枚举的实例属性(实例和原型)

还可以使用Object.getOwnPropertyNames,这个获取的是所有的实例属性,包括不可枚举的。

更简单的原型语法

function Person() {}

Person.prototype = {
    name: 'len',
    age: 23,
    sayName: function() {
        return this.name
    }
}
复制代码

上面代码有个例外,因为我们重写了Person.prototype,所以constructor不再指向Person,尽管 instanceof操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了,如下所示。

let p1 = new Person();
console.log(p1 instanceof Object) //true
console.log(p1 instanceof Person) //true
console.log(p1.constructor == Person) //false
console.log(p1.constructor == Object) //true
复制代码

如果constructor很重要,我们可以这样

Person.prototype = {
    constructor: Person,
    name: 'len',
    age: 23,
    sayName: function() {
        return this.name
    }
}
复制代码

这样constructor的enumeration会被设置为true,我们可以通过Object.defineProperty()来设置为false

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
})
复制代码

原型的动态性

var p1 = new Person()
// 这里没有重写
Person.prototype.sayHi = function(){
    console.log("hi")
}
p1.sayHi() //"hi"(没有问题!)

再看这个

function Person(){
}
var p1 = new Person()
// 这里重写了
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        return this.name
    }
}
p1.sayName() //error
复制代码

为什么这里报错了呢?是因为p1指向的原型中不包含以该名字命名的属性。重写原型对象切断了现有原型与任何之前已存在的对象实例之间的联系,他们应用的任然是最初的原型。

原型对象的问题,所有的实例和方法都是共享的,当然,你可以重新覆盖之前的属性。但是,对引用类型的话,就没这那么好受了,请看

function Person() {
}

Person.prototype = {
    name: 'len',
    age: 23,
    loveColors: ['white', 'black', 'red']
}

let p1 = new Person()
let p2 = new Person()

p1.loveColors.push('yellow')

console.log(p1.loveColors) // ['white', 'black', 'red', 'yellow']
console.log(p2.loveColors) // ['white', 'black', 'red', 'yellow']
console.log(p1.loveColors === p1.loveColors) // true
复制代码

我们怎样才能做到引用类型的私有化呢?这就是我们下面要说的。

组合使用构造函数模式和原型模式, 废话不多说,直接上代码

function Person(name, age) {
    this.name = name
    this.age = age
    this.loveColors = ['black', 'white']
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        return this.name
    }
}

let p1 = new Person()
let p2 = new Person()

p1.loveColors.push('red')

console.log(p1.loveColors) // ['black', 'white', 'red']
console.log(p2.loveColors) // ['black', 'white']

console.log(p1.loveColors === p2.loveColors) // false
console.log(p1.sayName === p2.sayName) // true
复制代码

这种方式是目前使用最广泛、认同度最高的一种创建自定义类型的方法

下面还有两种模式供参考

动态原型模式

function Person(name, age) {
    this.name = name
    this.age = age
    
    if (typeof this.sayName !== 'function') {
        Person.prototype.sayName = {
            return this.name
        }
    }
}
复制代码

这里只有在sayName不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。

唯一需要注意的是:不能使用字面量重写原型。在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的关系。

寄生构造函数模式

function Person(name, age) {
    let obj = new Object()
    obj.name = name
    obj.age = age
    obj.sayName = function() {
        return this.name
    }
    return obj
}

let p = new Person('len', 23)

console.log(p.sayName()) // len

这种模式其实和工厂函数一模一样,不同的在于生成实例的时候,这里使用了new操作符。这个模式可以在特殊的情况下用来为对象创建构造函数。

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");
alert(colors.toPipedString()); //"red|blue|green"
复制代码

稳妥构造函数模式

function Person(name, age) {
    var obj = new Object()
    obj.name = name
    obj.age = age
    obj.sayName = function() {
        return this.name
    }
    return obj
}

let p = Person('len', 23)
console.log(p.sayName) // len


这种模式有两个限制,不能在构造函数内使用this,不能使用new生成实例。比较适用于一些安全的环境中。
复制代码

猜你喜欢

转载自juejin.im/post/5bfe3ce0e51d453a6800a69a