理解对象
对象是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操作符的时候,浏览器做了以下工作:
- 在内存中创建一个新对象
- 将对象的[[prototype]]特性赋值为构造函数的prototype。
- 构造函数内部的this被修改为新创建的对象。
- 执行构造函数内的代码。
- 如果构造函数内有返回值,则接受,否则返回新创建的对象。
上面创建的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等原生的原型对象中添加方法,但不推荐这样做,可能引发很多问题。
推荐的做法是,创建自定义的类继承原生的原型对象。
原型的问题
- 弱化了传递初始化参数的能力,使得所有的对象初始值都一样。
- 原型对象中的引用数据类型会公用,可能出现问题。
对象迭代
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-in 和 Object.keys() 枚举顺序不确定,因浏览器而异。
- Object.getOwnPropertyNames 和 Object.getOwnProtertySymbols() 的枚举顺序遵守下面的规则:
- 先升序枚举数值键
- 以插入顺序枚举字符串和符号键
迭代方法
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;
}
}