JS高级---面向对象

理解对象

对象是Object的实例,js中万物皆对象,都拥有属性方法

比如下面的两个对象通过不同的方式创建,但他们是等价的

// 实例化对象
let p1 = new Object();
p1.name = "neko";
p1.job = "anchor";
p1.sayName = function() {
    console.log(this.name);
}
// 字面量对象
let p2 = {
    name : "neko";
    job : "anchor";
    sayName() {
        console.log(this.name);
    }
}

属性的类型

数据属性

数据属性包含一个保存数据值的地方,值会在这里读取和写入,拥有下面的4个特性。

  • Configurable: 属性是否可以被删除和重新定义,是否可以修改其他特性,默认值为true。
  • Enumerable:属性是否可以通过for-in循环,默认值为true。
  • Writable:属性是否可以被修改,默认值为true。
  • value:属性的值。

特性是被封装起来的,要修改数据的特性,需要调用Object.defineProperty() 方法,这个方法接受3个参数

  • 第一个参数是要修改的对象。
  • 第二个参数是属性的名称。
  • 第三个参数是特性组成的对象。

当Configurable被修改为false时,这个对象就会被锁死,不能再调用**Object.defineProperty()**方法。

访问器属性

访问器属性不包含属性值,他可以用来设置和获取对象的数据属性。

他拥有四个特性

  • Configurable:表示是否可以被删除和修改,是否可以修改特性,是否可以可以转化为数据属性,默认值为true。
  • Enumereable:表示属性是否可以被遍历,默认值为true。
  • Get:获取函数,用于读取属性值,默认值为undefined。
  • Set:设置函数,用于写入属性值,默认值为undefined。

访问器属性不能直接定义,只能通过**Object.defineProperty()**方法定义。

let book = {
    year_ : 2017,
    edition : 1;
};

Object.defineProperty(book , 'year' , {
    get(){
        return year_;
    },
    set(val){
    	this.year = val;
        this.edition = val - this.edition;
	}
})

book.year = 2018;
console.log(book.edition)

其中get函数用于获取值,只是简单得让year的值和year_在数值上相同。

set函数用于设置值,当year的值变化时,会将新的值作为参数传入set函数,然后调用。(我怀疑vue就是用这东西搞的)

set和get都是可选的,只定义get意味着属性是只读的,不能修改,只有set函数则不能进行值的读取,必须做赋值操作。

静态方法

定义多个属性

**Object.defineProperty()**可以同时为对象定义多个属性,包括数据属性和访问器属性,语法如下。

let girl = {};
Object.defineProperty(girl , {
    name : {
        value : 'neko';
    },
    age : {
        vallue : 18;
    },
    othername : {
        get(){
            retuen this.name;
        },
        set(val){
            this.name = val;
        }
    }
})

这样与单独定义的区别是:属性特性的默认值都会变为false。(configurable、enumerable、writable)

特性读取

使用**Object.getPropertyDescriptor()**可以获取到指定属性的修饰符。

这个函数接受两个参数,第一个参数是属性所在的对象,第二个参数是属性名。

返回值是一个由属性特性组成的对象。

let book = {};
Object.defineProperty(book , {
    year_ : {
        value : 2022,
    },
    edition : {
        value : 1,
    },
    year : {
        get(){
            return this.year_;
        },
        set(val){
            this.edition = val;
        }
    }
})

let ds = Object.getPropertyDescriptor(book , "year_");
//{
//    value : 2022,
//    configurable : false,
//    enumerable : false,
//    writable : false,
//}

ES6在2017年新增加了**Object.getPropertyDescriptors()**方法,这个函数接受一个目标对象,返回一个属性对象,属性对象内有各个属性的特性值组成的对象。

这个方法的本质是对**Object.getPropertyDescriptor()的再封装,就是对象的每个属性使用Object.getPropertyDescriptor()**方法。

let book = {};
Object.defineProperty(book , {
    year_ : {
        value : 2022,
    },
    edition : {
        value : 1,
    },
    year : {
        get(){
            return this.year_;
        },
        set(val){
            this.edition = val;
        }
    }
})

let ds = Object.getPropertyDescriptors(book);
//{
//    year_ : {...},
//    edition : {...},
//    year : {...},
//}

对象合并

将源对象的属性和方法复制到目标对象上称为对象合并,也叫做混入(mixin);

可以使用**Object.assign()**方法。

这个方法第一个参数为要接受合并的目标对象,之后跟上若干个源对象,然后将源对象中的可枚举属性和自有属性拷贝到目标对象。

这个方法只能实现浅拷贝,也就是拷贝地址,拷贝后的属性只是之前的引用。

let dest = {};
let src = { a : {} }

Object.assign(dest , src);
console.log(dest.a === src.a) // true

合并后的源对象属性会覆盖掉目标对象的同名属性。

let a = {id : 1 , name : 'neko'};
let b = {id : 2 , age : 16};

Object.assign(a , b);
console.log(a) //{id : 2 , name : 'neko' , age : 16}

如果在拷贝期间出错,会立刻停止执行,不会回滚到执行之前。

对象相等

新增加了Object.is方法,可以检测两个对象的内容是否相等。

这个方法接收两个被比较的对象,返回一个布尔值。

由于这个函数比较的是内容,所以可以弥补===的不足。

例如NaN和NaN的比较,对象的比较。

如果有多个要检查的对象,可以自定义函数利用递归比较即可

function reis(x , ...y){
    return Object.is(x , y[0]) &&
        (y.length < 2 || reis(...y))
}

对象的增强写法

属性值简写

有的时候属性值(变量名)和属性名是一样的,这时候就可以简写。

下面的两种写法是一样的。

let name = "neko";
// 之前的写法
const a1 = {
    name : name,
}
// 增强写法
const a2 = {
    name,
}

可计算属性

可计算属性简化了动态属性名的定义。

下面的两种写法是一样的。

// 之前的写法
let o = {
	name : 'name',
    age : "age",
};

let p1 = {
    o[name] : 'neko',
    o[age]18,
}
// 增强写法
let name = "name",
let age = "age",
let p2 = {
    [name] : 'neko',  // 将属性名用中括号包裹起来,可以将其作为js表达式来求解。
    [age] : 18,
}

简化方法名

在对象内定义函数时也有增强写法。如下所示。

let p = {
    // 过去的写法
    fun1 : function(){
        
    },
    // 增强写法
    fun2(){
        
    }
}

注意函数的增强写法和可计算属性是兼容的。

let fun1 = 'NekoSay';

let p = {
    [fun1](){
        console.log("哪来的野猫呀【该用户已被禁言】")
    }
}

p.NekoSay(); // 哪来的野猫呀【该用户已被禁言】

对象解构

对象解构是用来将对象中的属性抽出变为单独变量的操作。

这是es6中新增加的语法,下面两种写法的效果是一样的。

let o = {
    name : 'neko',
    age : 16,
};
// 之前的写法
let girlName = o.name;
let girlAge = o.age;
// es6写法
let { name : girlName , age : girlAge } = o;

如果想使用的变量名和属性名相同,可以使用简写。

let {name , age} = o;

如果对象中没有想要结构的值,就会赋值为undefined,也可以在解构时附上默认值。

同时如果出现对象中不存在的值,解构喙中途停止,也不会有回滚操作。

let {name , sex = "girl"} = o // 因为sex不存在所以直接用默认值,没有默认值就变为undefined,后面的其他属性也会停止解构

被解构的值会被转化为对象,即使是数字,字符串或布尔值也可以被解构,但是null和undefined不可以。

let {length} = "foobar" // 6
let { _ } = null // TypeError

如果是为事先声明过的值解构,需要加上小括号。

let oName , oAge;
(let { name : oName , age : oAge } = o);

解构赋值可以嵌套使用

let o = {
    name : 'neko',
    job : {
        title : "player"
    }
}

let { job {title} } = o;
console.log(title) // player

解构赋值也可以在函数中使用

function printPerson(foo , {name , age} ,bar) {
    console.log(arguments); // 打印接收到的所有参数
    console.log(name,age);
};

printPerson("111", {name : 'neko' , age : 16} , "222"); 
// ["111", {name : 'neko' , age : 16}, "222"]
// {name : 'neko' , age : 16}

创建对象

在es6引入类之后对于对象创建的优化算是结束了。但是在类之前就有许多创建对象的方案,类只不过是简化这些方案。

下面依次介绍被替代掉的这些方案。

工厂模式

工厂函数是一种最早的设计模式,他的示例语法如下:

function createPerson(name , age){
    let o = new Object();
    o.name = name;
    o.age = age;
    return o;
}

let p1 = createPerson('neko' , 16);

这样虽然可以模板化得创建对象,但是存在诸多问题,比如没有对象标识符(也就是对象的类型)

构造函数模式

js中提供了一些原生的构造函数,例如Object()Array(),我们也可以自定义构造函数。

下面是构造函数的语法示例:

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

let p1 = new Person('neko' , 16);
let p2 = new Person('amiya' , 16);

按照惯例,我们将构造函数的首字母大写来和普通函数做区分。

在创建实例时,需要使用new关键字来创建,要注意构造函数也是个函数,他和普通函数的区别只是调用方式的不同,它通过new关键字来调用。

构造函数也可以当作普通法函数来调用,这样就会在window对象上挂在相应的属性和方法。

Person('neko' , 16);
console.log(window.name) // neko

在使用new操作符的时候,浏览器做了以下工作:

  1. 在内存中创建一个新对象
  2. 将对象的[[prototype]]特性赋值为构造函数的prototype。
  3. 构造函数内部的this被修改为新创建的对象。
  4. 执行构造函数内的代码。
  5. 如果构造函数内有返回值,则接受,否则返回新创建的对象。

上面创建的p1和p2中保存有不用的实例,但他们的constructor属性都指向Person构造函数。

也可以使用instanceof操作符来获取对应的构造函数,也就是对象类型。

p1.constructor === p2.constructor === Person  // true
p1 instanceof Person  // true
p1 instanceof Object  // true

构造函数同样存在很多问题:构造函数的每一个实例都是独立的,有一些重复的属性和方法,也都会生成两份,造成内存浪费。

原型模式

要理解原型模式,需要先理解原型。

理解原型

要理解原型就是是理解原型对象,构造函数和实例对象的关系。

原型之间的关系比较复杂,简单画一张图理解一下。

在这里插入图片描述

  • 类中有一个prototype属性指向原型对象。
  • 原型对象中有一个constructor属性指向类。
  • 实例对象中的prototype也指向原型对象,但这个属性被封装,只能通过__proto__属性来指向原型。
  • 在创建类时会创建出一个原型对象,实例对象共有的属性和方法会储存在原型对象中。

ES6中提供了 Object.getPrototypeof() 方法返回实例对象内部的 [[prototype]] 属性的值,使用这个方法可以取得一个实例对象的原型。

ES6中还提供了 Object.setPrototypeof() 方法,可以向实例对象内部的 [[prototype]] 写入值修改其对应的实例对象,但这个方法严重影响性能,不建议使用。要实现其对应的功能,可以通过 Object.create() 方法创建对象,并为其指定原型。

let a = {
	age : 16
}; // 模拟一个原型对象

let b = Object.create(a); // 创建新对象b,将a指定为原型

原型层级

在通过对象访问属性时,会通过属性名进行查找,如果在实例对象上找不到对应的属性名,则会查找原型对象。

实例对象上的属性会覆盖掉原型对象上的同名属性。

通过调用hasOwnProperty() 方法可以确定某个属性,是来自实例对象,还是来自原型对象。

function Person () {};
Person.prototype.name = 'neko';

let p1 = new Person();
p1.age = 16;

p1.hasOwnProperty('name'); // 来自原型,返回false
p1.hasOwnProperty('age'); // 来自实例,返回true

重写原型对象

如果原型模式中包括多个对象,重复的出现propotype显得冗余,可以采用下面的方法进行重写。

function Person () {};
Person.prototype = {
	name: 'neko',
	age: 16
}

这样做会导致新的原型的constructor属性指向Object而不是Person,
可以在重写时指定constructor属性。

function Person () {};
Person.prototype = {
	constrcutor: Person,
	name: 'neko',
	age: 16
}

这样做依然有问题,原生的constructor属性时不可枚举的,因此应当使用 Object.defineProperty() 方法来定义constructor属性。

function Person () {};
Person.prototype = {
	name: 'neko',
	age: 16
}

Object.defineProperty(Person.prototype , "constructor" , {
	enumrable : false,
	value : Person
})

重写之后的原型将不能再影响重写前创建的对象。
之前的对象依然指向重写前的原型。

在对象创建之后修改原型,实例对象的值也会随之变化。

原生对象模型

通过prototype可以向String,Array等原生的原型对象中添加方法,但不推荐这样做,可能引发很多问题。

推荐的做法是,创建自定义的类继承原生的原型对象。

原型的问题

  1. 弱化了传递初始化参数的能力,使得所有的对象初始值都一样。
  2. 原型对象中的引用数据类型会公用,可能出现问题。

对象迭代

for-in

  • for-in 循环可以获得对象中所有的可枚举的属性(包括被封装的属性)

  • Object.keys() 方法可以实现类似的效果,不过他只能放回实例对象上的属性,不包含原型对象上的属性。

  • Object.getOwnPropertyNames 可以获得实例对象上所有属性,无论其是否可以枚举

  • Object.getOwnProtertySymbols() 方法可以获得对象上所有的以符号定义的属性名。

function Person() {};

Person.prototype.name = 'neko';

let p1 = new Person();
p1.age = 16;

const s1 = Symbol('s1');
let o = {
  [s1] : 's1',
  s2 : 's2'
}
console.log(Object.keys(p1)); // [ 'age' ]
console.log(Object.getOwnPropertyNames(p1)); // [ 'age' ]
console.log(Object.getOwnPropertyNames(Person.prototype)); // [ 'constructor', 'name' ]
console.log(Object.getOwnPropertySymbols(o)); // [ Symbol(s1) ]

对于以上的枚举方法,枚举顺序有所差异。

  • for-inObject.keys() 枚举顺序不确定,因浏览器而异。
  • Object.getOwnPropertyNamesObject.getOwnProtertySymbols() 的枚举顺序遵守下面的规则:
    1. 先升序枚举数值键
    2. 以插入顺序枚举字符串和符号键

迭代方法

ES6中新增两个静态方法用于对象迭代(取得对象中各属性的值)

  • Object.values()
  • OBject.entries()

这两个方法在进行对象迭代时,会忽略符号键。

const girl = {
  name : 'neko',
  age : 16,
  [Symbol('k1')] : 'k1',
}

console.log(Object.values(girl)); // [ 'neko', 16 ]
console.log(Object.entries(girl)); // [ [ 'name', 'neko' ], [ 'age', 16 ] ]

原型链

(这里有很多东西我看不懂,就没有写出来,暂时先这样)

js通过原型链实现了继承。
如果一个原型对象是另一个原型对象的实例就构成了原型链。
在这里插入图片描述
当son实例需要一个属性时,会现在自身检索。
如果找不到则通过__proto__向Son原型寻找。
如果还找不到,则再通过__proto__向Father原型寻找。

Son原型是Father原型的实例对象,重点在于Son类没有使用默认原型,而是将原型替换为了Father类的实例对象,这样做实现了继承,实例对于属性的搜索会一直持续到原型链末端。

默认原型

默认情况下所有的引用类型原型链末端都是Object原型对象,

原型与继承

确定一个实例与原型的关系有两种方法

  • instanceof操作符
  • isPrototypeOf() 原型方法
let str = 'aaa';

// 只要在原型链中出现过就返回true
str instanceof String // true
str instanceof Object // true

// 只要在原型链中出现过就返回true
String.prototype.isPrototypeOf(str) // true
Object.prototype.isPrototypeOf(str) // true

原型链的问题

原型链存在的问题有两个:

  • 首先依然是引用数据类型的问题。
  • 子类在实例化时,无法影响父类的构造函数。

前面使用各种方法来模拟类的行为。
es6正式引入了类,但其背后依然是使用前面的方法,本质上只是一个语法糖。
前面的作为了解,因为有了类以后不再需要那些东西,但那些东西能帮我们理解类的原理。

类的定义

按照下面的语法来定义类

class Person {

}

注意:

  • 类不能被提升,在实例化之前不能引用。
  • 类可以由构造函数、实例方法、获取函数、设置函数、静态方法组成,但这些都不是必须的,可以定义空类。

构造函数

constructor关键字用于在类中定义构造函数。
在使用new关键字创建实例时会调用这个函数,默认会自动创建一个空的构造函数。

在使用new实例化类时,可以传入参数,这些参数会传入构造器函数中来初始化对象。

class Girl {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
}

const m = new Girl('neko' , 16);
// {name:'neko' , age:16}

构造器函数就是一个特殊的函数,实例化之后也会变为普通函数,只不过在调用时依然需要使用new关键字。

类也是一个特殊的函数,之前的原型链之间的关系在类这里依然适用。

实例、原型和类

通过构造器创建出的实例中的成员都是独立的,不会共享。

为了在实例之间共享方法,可以在类中定义方法,这些方法会被附加在原型上。

class Girl {
	constructor(name,age){
		// 这些都是独立的
		this.name = name;
		this.age = age;
	}
	// 这个方法会出现在原型上。
	say() {}
	// 特别注意不能再类中添加属性。
}

const m = new Girl('neko' , 16);
// {name:'neko' , age:16}

类也支持获取get和set函数,语法跟普通对象一样。

class Girl {
	set name(newVal){
		this.name_ = newVal;
	}

	get name(){
		return this.name_;
	}
}

const m = new Girl();

类可以定义静态方法,使用static关键字,静态方法只能由类本身来调用,一个类只能由一个静态方法。

class Girl {
	static say(){}
}

Girl.say();

虽然类在定义是不允许添加属性和方法,但可以在外部进行添加。

class Girl {
	
}

Girl.age = 16; // 在类上添加
Girl.prototype.age = 18; // 在原型对象上添加

继承

使用extends关键字进行继承操作,不仅可以继承类,还可以继承构造函数。

super

子类可以在构造函数和静态方法中使用super,super必须写在第一行。

在构造函数中使用super会调用父类的构造函数。
在静态方法中使用super会调用父类的静态方法。

class Girl {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	static say() {}
}

class GirlFriend extends Girl {
	constructor(name,age){
		super(name,age);
	}

	static cry(){
		super.say();
	}
}
const m = new GirlFriend('neko' , 16);
// {name:'neko' , age:16}

抽象基类

有时候会需要一个不会被实例化,但需要被继承的类,这种类称为抽象基类。
js没有提供相应的语法,但可以通过 new.target 实现。

class Base {
	constructor(){
		if(new.target === Base){
			throw new Error("不能创建抽象基类")
		}
	}
}

在抽象基类中可以进行检查,要求子类必须包含某些方法。

class Base {
	constructor(){
		if(new.target === Base){
			throw new Error("不能创建抽象基类")
		}

		if(!this.foo){
			throw new Error("子类中必须有自己的foo方法")
		}
	}
}

继承内置类型

继承js内置的类可以便捷的扩展功能

class MyArray extends Array {
	myfun(){}
}

有些内置的方法会放回新的实例,默认会通过调用者的类来创建新的实例。

const a1 = new MyArray(1,2,3,4,5);

let a2 = a1.filter(x => !!(x % 2)) // 返回值为MyArray类型

通过覆盖Symbol.species访问器可以修改这一行为

class MyArray extends Array {
	static get [Symbol.species] () {
		return Array;
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值