JS基础总结(2)——对象和类

1. 对象

对象: 无序属性的集合,属性可以包含基本值、对象或者函数。

1.1 对象属性

1.1.1 数据属性

数据属性:包含一个数据值的位置,可以在这个位置读取和写入值
属性描述符:

  1. Configurable: 能否删除或重新定义或修改属性与属性特性,默认true
  2. Enumerable: 能否遍历访问,默认true
  3. Writable: 能否修改值,默认true
  4. Value: 属性值,默认undefined

1.1.2 访问器属性

访问器属性: 不包含数据值,包含操作函数getter和setter
属性描述符:

  1. Configurable: 能否删除或重新定义或修改属性与属性特性,默认true
  2. Enumerable: 能否遍历访问,默认true
  3. Get: 读取属性时调用的函数,默认undefined
  4. Set: 写入属性时调用的函数,默认undefined

1.1.3 属性方法

  1. Object.defineProperties() 通过描述符一次定义多个属性
  2. Object.getOwnPropertyDescriptor() 获取指定属性的描述符

1.2 操作对象

1.2.1 定义

最基本的定义方式,其他复杂的基本都基于此

const obj = {} 
const obj = new Object({})
1.2.2 赋值

‘=’ 即可赋值,由于是引用数据类型,传递的是地址,因此obj1和obj2看起来一样,实际不相等

const obj1 = {}, obj2 = {}
const obj3 = obj1
console.log(obj1 === obj2) // false
1.2.3 操作

对象的操作基于地址,会有这类数据的通病

  1. 弱不变性
const obj = {
    list: []
}
Object.freeze(obj)
obj.list.push(1)
console.log(obj); //{ list: [1] }
  1. 互相影响
const obj1 = {}
const obj2 = obj1
obj1.a = 1
console.log(obj2) // { a: 1 }
  1. 深浅拷贝

浅拷贝: 只拷贝一层
深拷贝: 拷贝所有层

const obj1 = { list: [] }
const obj2 = Object.assign({}, obj1) // 浅拷贝
const obj3 = JSON.parse(JSON.stringify(obj1)) // 深拷贝
obj1.list.push(1)
console.log(obj2, obj3) // { list: [1] }  list: [] }

2. 类

2.1 实现方式

在此只讨论两种与下文相关的类的实现方式

2.1.1 构造函数模式
function Hero(name, job) {
    this.name = name
    this.job = job
    this.profile = function() {
        console.log(this.name + ' is a(n) ' + this.job)
    }
}
const Teemo = new Hero('Teemo', 'ATM')
Teemo.profile() // Teemo is a(n) ATM
const Soraka = new Hero('Soraka', 'support')
Soraka.profile() // Sorakais a(n) support

在这个例子中,Teemo和Soraka都是从Hero构造函数中创建出来的实例,它们都拥有两个属性以及一个方法。但是存在一个不完美的地方,那就是profile方法被构建了两次,由于这个方法对于所有实例来说行为都是一致的,没必要重复构建,因此会造成资源的浪费。

2.1.2 原型模式

JS中创建的每一个函数都有一个prototype属性,指向一个对象(即原型),这个对象可以包含由特定的所有实例共享的属性和方法。因此不必在构造函数中定义对象实例的信息,可以直接将信息添加到原型对象中,如下:

function Hero() {}
// 方法一
Hero.prototype.name = 'Teemo'
Hero.prototype.job = 'ATM'
Hero.prototype.profile = function() {
    console.log(this.name + ' is a(n) ' + this.job)
}
/*
方法二
Hero.prototype = {
	constructor: Hero, // 由于重新定义了原型对象,需要声明constructor属性保持原型与构造函数关系的一致
	name: 'Teemo',
	job: 'ATM',
	profile: function () {
		console.log(this.name + ' is a(n)' + this.job)
	}
}
*/
const Teemo = new Hero()
Teemo.profile() // Teemo is a(n) ATM
const Soraka = new Hero()
Soraka.profile() // Teemo is a(n) ATM

上面例子也创建了两个实例Teemo和Soraka,然而profile打印出来的内容是一样的。的确,我们可以使用原型链的覆盖来为实例添加属性,例如:

const Soraka = new Person()
Soraka.name = 'Soraka'
Soraka.job = 'support'
Soraka.profile() // Soraka is a(n) support

但是如果说存在引用数据类型的情况,我们在原型链中加一条属性position:

Hero.prototype.position= ['top', 'sup'] // 提莫和索拉卡都可以打上路和辅助
const Teemo = new Person()
const Soraka = new Person()
Teemo.position.push('jungle') // 提莫还可以打野,把打野加进去
console.log(Soraka.position) // ['top', 'sup', 'jungle']

这样的话一旦任意一个实例修改了引用数据类型里面的值,其他所有实例都会被影响,这显然不是我们希望看到的。

2.1.3 构造函数模式+原型模式

上述两种方法各有各的优势也各有各的不足,所以理所当然的各自取长补短诞生了最常见的实现方式,将两者结合,构造函数用于定义实例的属性,原型链用于定义实例的方法。代码如下:

function Hero(name, job) {
    this.name = name
    this.job = job
}
Hero.prototype = {
    constructor: Hero,
    profile: function () {
    	console.log(this.name + 'is a(n)' + this.job)
	}
}
const Teemo = new Hero('Teemo', 'ATM')
Teemo.profile() // Teemo is a(n) ATM
const Soraka = new Hero('Soraka', 'support')
Soraka.profile() // Sorakais a(n) support

2.1.4 Class

在ES6之前,最为常用的类的构建方式便是2.3中所讲述的方法了,然而在ES6出来之后,JS原生提供了Class这个语法来帮助开发者定义类,因此使用ES6后类的定义变的非常简单了,基本与其他存在类的概念的语言相同,例如:

class Hero {
    constructor(name, job) {  // 类似于构造函数
        this.name = name
        this.job = job
    }
    profile () {
        console.log(this.name + 'is a(n)' + this.job)
    }
}
const Teemo = new Hero('Teemo', 'ATM')
Teemo.profile() // Teemo is a(n) ATM
const Soraka = new Hero('Soraka', 'support')
Soraka.profile() // Sorakais a(n) support

2.2 继承

ES6之前,最理想的继承方式是寄生组合式继承

function inherit (child, parent) {
    const currentPrototype = Object.create(parent.prototype)
    currentPrototype.constructor = child
    child.prototype = currentPrototype
}
function Creature (name, gender) {
    this.name = name
    this.gender = gender
}
Creature.prototype.introduction = function () {
    console.log('my name is ' + this.name, ', I am ' + this.gender)
}
function Hero (name, gender, job) {
    Creature.call(this, name, gender)
    this.job = job
}
inherit(Hero, Creature)
const Teemo = new Hero('Teemo', 'male', 'ATM')
Teemo.introduction() // my name is Teemo , I am male

ES6提供的class

class Creature {
    constructor (name, gender) {
        this.name = name
        this.gender = gender
    }
    introduction () {
        console.log('my name is ' + this.name, ', I am ' + this.gender)
    }
}
class Hero extends Creature{
    constructor (name, gender, job) {
        super(name, gender)
        this.job = job
    }
    profile () {
        console.log('My job is to be a(n) ' + this.job)
    }
}
const Teemo = new Hero('Teemo', 'male', 'ATM')
Teemo.introduction() // my name is Teemo , I am male
Teemo.profile() // My job is ATM

多继承:Creature 类继承自 Unit类,然后Hero 类继承自Creature类

2.3 多态

ES6之前的实现方式:

function inherit (child, parent) {
    const currentPrototype = Object.create(parent.prototype)
    currentPrototype.constructor = child
    child.prototype = currentPrototype
}
function Creature (name, gender) {
    this.name = name
    this.gender = gender
}
Creature.prototype.introduction = function () {
    console.log('my name is ' + this.name, ', I am ' + this.gender)
}
function Hero (name, gender, job) {
    Creature.call(this, name, gender)
    this.job = job
}
inherit(Hero, Creature)
Hero.prototype.introduction = function () {
    console.log('my name is ' + this.name, ', I am ' + 
    this.gender + ', I should be used as a(n) ' + this.job)
}
const Teemo = new Hero('Teemo', 'male', 'ATM')
Teemo.introduction() // my name is Teemo , I am male, I should be used as a(n) ATM

class的实现方式:

class Creature {
    constructor (name, gender) {
        this.name = name
        this.gender = gender
    }
    introduction () {
        console.log('my name is ' + this.name, ', I am ' + this.gender)
    }
}
class Hero extends Creature{
    constructor (name, gender, job) {
        super(name, gender)
        this.job = job
    }
    introduction () {
        console.log('my name is ' + this.name, ', I am ' +
            this.gender + ', I should be used as a(n) ' + this.job)
    }
}
const Teemo = new Hero('Teemo', 'male', 'ATM')
Teemo.introduction() // my name is Teemo , I am male, I should be used as a(n) ATM

3. 原型链

原型链完整图示
如图所示,整个原型链的逻辑便是如此,没有什么好说的,能完整画出这个图就说明弄明白了。
需要注意以下两点:

  1. 实例中并不存在__proto__(书上说的)和constructor属性,这两个属性都存在与原型中(原型和实例我都没找到__proto__属性,并不能肯定__proto__不存在于实例是正确的。个人倾向于存在,否则如何将实例与原型相关联?)
  2. Function的构造函数和Object的原型是特例(原型链的终点),Function的构造函数的__proto__指向Function的prototype,Object的prototype的__proto__指向null

4. 行为委托

相比于继承来说,委托这个词更为恰当,两者之间有这非常明显的差别:
类设计: 类会复制父类的所有属性和方法。构造完成后,通常只需要操作实例并且每个实例都拥有需要完成的任务的所有行为。
委托设计: 类不会复制父类中的属性和方法(构造函数中除外)。构造完成后,实例只包含完成任务的部分数据(实例属性),并不包含完成任务的全部数据(原型属性)和行为,当在当前对象中找不到所需要的资源,将会沿着原型链向上查找。
这是一种极其强大的设计模式,和父类,子类,继承,多态等概念完全不同,通过使用这种模式,将不再局限于父类到子类之间的垂直关系组织,而是可以通过任意方向的委托关联并排组织,例如:

const Teemo = {
    plantMushroom () {
        console.log('you plant a plantMushroom')
    }
}
const Soraka = {
    heal () {
        console.log('you have been healed')
    }
}
const nami = {
    bubble () {
        console.log('you have thrown a bubble')
    }
}
Object.setPrototypeOf(nami, Teemo)
Object.setPrototypeOf(Soraka, Teemo)
Object.setPrototypeOf(nami, Soraka)
nami.plantMushroom() // you plant a plantMushroom
nami.heal() // you have been healed
Soraka.plantMushroom() // you plant a plantMushroom

5. Class

5.1 使用方法

  1. 通过class关键字定义,通过new 操作符来创造实例

  2. constructor: 类似于构造函数,new调用时自动调用该方法,如未被定义则默认添加

  3. new: 生成实例必用,否则报错

  4. Class表达式:

const myClass = class Me {} // Me 代指当前类,只在Class内部有定义
  1. 不存在变量提升,提前调用报错

5.2 属性和方法

5.2.1 静态属性和方法

属性分为类的属性和实例属性,在Class内部默认都是实例属性,当在属性前加上static时,该属性则为类属性

class myClass {
    static property = 'class'
    prop = 'instance'
    constructor () {}
}
const instance = new myClass()
console.log(instance.prop, instance.property, myClass.property) // instance undefined class

类中定义的方法都会被实例所继承,当在方法前加上static时,该方法直接通过类调用,无法被实例继承

class myClass {
    static property () {
        console.log('class')
    }
    prop () {
        console.log('property')
    }
}
const instance = new myClass()
instance.prop() // class
myClass.property() // property
instance.property() // 报错
5.2.2 存取值函数

可以在class内部对某个属性设置存取值函数,拦截属性的存取行为

class myClass {
    _name = 'soraka'
    get name () {
        return this._name
    }
    set name(val) {
        this._name = val
    }
}
const instance = new myClass()
console.log(instance.name) // soraka
instance.name = 'teemo'
console.log(instance._name) // teemo

5.3 继承

5.3.1 实现方法

通过 extends关键字实现继承,子类必须在constructor方法中调用super方法,否则子类拿不到自己的this对象,造成新建实例报错。原因如下:

es5 中继承是先创建子类实例,然后将父类属性和方法添加进去
es6 中继承是先创建父类实例,然后用子类属性和方法进行覆盖

5.3.2 super

super 关键字既可以当函数使用也可以当对象使用

  1. 作为函数使用时,代表父类的构造函数,只能在子类的构造函数中调用,在其他位置会报错
  2. 作为对象使用时,代表父类的原型对象,在静态方法中指向父类

super 调用父类方法时,会绑定子类的this,因此当方法修改对象属性时,会修改子类属性而不是父类

当使用super时,必须显示定义将super当做对象还是方法使用,否则会报错

发布了6 篇原创文章 · 获赞 0 · 访问量 84

猜你喜欢

转载自blog.csdn.net/weixin_44844528/article/details/104283921