JavaScript 的面向对象(OO)

栏目: 后端 · 前端 · 发布时间: 6年前

内容简介:在面向对象编程的语言中,都有类的概念,可以基于这个类创建无数个拥有相同属性和方法的对象。在js中,是没有类的概念的,所以会略有所不同。对象: {} 这就是一个对象,对,没错,就是这么简单。我们可以将对象想象成一个散列表,无非就是一些键值对,值可以是数据或函数。每一个对象都是基于引用类型创建的,可以是原生(基本、引用)类型,也可以是自定义类型。基本类型(按值访问): Number, String, Undefined, Unll, Boolean, Symbol.

在面向对象编程的语言中,都有类的概念,可以基于这个类创建无数个拥有相同属性和方法的对象。在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
复制代码

和工厂函数的不同

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
}
复制代码

构造函数的调用方式

let p = new Person()
let p = Person()
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生成实例。比较适用于一些安全的环境中。
复制代码

以上所述就是小编给大家介绍的《JavaScript 的面向对象(OO)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First Servlets & JSP(中文版)

Head First Servlets & JSP(中文版)

(美)巴萨姆、(美)塞若、(美)贝茨 / 苏钰函、林剑 / 中国电力出版社 / 2006-10 / 98.00元

《Head First Servlets·JSP》(中文版)结合SCWCD考试大纲讲述了关于如何编写servlets和JSP代码,如何使用JSP表达式语言,如何部署Web应用,如何开发定制标记,以及会话状态、包装器、过滤器、企业设计模式等方面的知识,以一种轻松、幽默而又形象的方式让你了解、掌握servlets和JSP,并将其运用到你的项目中去。《Head First Servlets·JSP》(中......一起来看看 《Head First Servlets & JSP(中文版)》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具