js对象
基础操作
创建自定义对象的方式通常是创建一个Object的新实例,然后添加属性和方法let person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'engineer';
person.sayName = function()
{
console.log(this.name);
}
这是早期创建对象的方式。如果使用对象字面量可以这样写let person =
{
name: 'Nicholas';
age: 29;
job: 'engineer';
sayName()
{
console.log(this.name);
}
}
属性类型
属性有些是对象中的成员变量,还有些类似于c#中的属性(property).这些属性都有一些内置的attribute(特性),我们可以修改这些特性。为了标识某一个内部特性,一般使用两个中括号括起来,例如{% post_link Enumberable %}
使用Object.defineProperty()
可以更改属性的特性
数据属性
数据属性就是平常的变量,它包含一个保存数据值的位置,只会从这个位置读取,也会写入到这个位置,它的特性为
{% post_link Configurable %}
: 是否可以通过delete删除并重新定义,是否可以修改特性,是否可以把它改为可访问,它是下面配置的基础。默认是true{% post_link Enumerable %}
: 是否可以通过for-in返回,默认情况下是true{% post_link Writable %}
: 属性是否可以被修改,如果设置为false可以给这个变量赋值但是赋值无效。默认情况下是true{% post_link Value %}
: 包含属性实际的值
修改特性可以同时设置一个或多个值let person = {};
Object.defineProperty(person, 'name',
{
writeable: false,
value: 'Nicholas'
});
console.log(person.name); //Nicholas
person.name = 'Greg';
console.log(person.name);//Nicholas
一个属性被设置为不可配置之后,就不可以设置为可配置了。let person = {};
Object.defineProperty(person, 'name',
{
configurable: false,
value: 'Nicholas'
});
Object.defineProperty(person, 'name',
{
configurable: true,
});//报错
访问器属性
访问器有一个getter和一个setter函数,但是和c#中的属性不完全一样,c#中属性可以默认内部有一个值,但是访问器只可以对其他的值进行操作
{% post_link configurable %}
: 和上面一样{% post_link Enumerable %}
:{% post_link Get %}
: 默认是undefined{% post_link Set %}
: 默认是undefined
访问器属性不能直接定义,必须使用Object.defineProperty()
let book = |
读取属性特性
使用Object.getOwnPropertyDescriptor(object_name, property_name)
获取属性的属性描述符,例如
let descriptor = Object.getOwnPropertyDescriptor(book, 'year_'); |
此外还有Object.getOwnPropertyDescriptors()
方法,他会返回每一个对象的特性
合并对象
- Object.assign(): 将一个目标对象和多个源对象进行合并。将每个源对象的可枚举(Object.propertyIsEnumerable())和自有(Object.hasOwnProperty())属性复制到目标对象.对于每个符合条件的属性,会使用源对象上的Post not found: get获得属性,然后使用目标对象的set设置属性
dest = {}; |
assign()是浅复制,也就是说只会复制对象的引用,他们指向内存中的位置是相同的。并且如果出错无法回滚。
增强语法
- 属性值简写
在给对象添加变量时,经常发现属性名和变量名是一样的为了简单,有一种简写为letname = 'Matt';
let person =
{
name: name;
}
console.log(person);let name = 'Matt';
let person =
{
name
} - 可计算属性
可计算属性是用动态赋予变量名,例如const nameKey = 'name';
const ageKey = 'age';
let uniqueToken = 0;
function getUniqueKey(key)
{
return `${key}_${uniqueToken++}`;
}
let person =
{
[getUniqueKey(nameKey)] = 'Matt';//变量名是会变的。如name_0,name_1
[getUniqueKey(ageKey)] = 27;
} - 简写方法名
在给对象定义方法时,通常要写方法名加冒号再加方法,现在可以直接使用方法名简写还可以用于get和set,并且方法名可以用可计算属性代替//原来
let person =
{
say_name: function(name)
{
console.log(`My name is ${name}`);
}
};
//现在
let person =
{
say_name(name)
{
console.log(`My name is ${name}`);
}
}let person =
{
name_: '',
get name()
{
return this.name_;
}
} - 对象解构
原来想要赋值必须一个个赋值,现在可以使用一条语句进行赋值//原来赋值
let personName = person.name;
let personAge = person.age;
//现在
let {name: personName, age: personAge} = person;创建对象
工厂模式
例如:function createPerson(name, age, job)
{
let o = new Object();
o.name = name;
o.ageg = age;
o.job = job;
o.sayName = function()
{
console.log(this.name);
};
return o;
}
let person = createPerson('a', 29, 'doctor');
构造函数模式
例如:function Person(name, age, job)
{
this.name = name;
this.age = age;
this.job = job;
this.sayName = function()
{
console.log(this.name);
}
}
let person = new Person('a', 29, 'doctor');
构造函数不需要显示的创建对象,并且没有return。它使用new新建对象并且构造函数最好首字母大写。
它的创建过程为:
- 在内存中创建一个新对象
- 将新对象内部的Post not found: Prototype特性赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为当前对象
- 执行构造函数内部代码(给对象添加属性)
- 如果构造函数返回非空对象,则返回该对象,否则返回新创建的对象
但是这种创建方式有一些问题。例如上例中的sayName,在js中函数实际上是一个对象,因此每定一个一个函数相当于创建了一个对象,这会带来空间的浪费,因此可以这样function Person(name, age, job)
{
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName()
{
console.log(this.name);
}
原型模式
每个函数都会创建一个prototype属性,这个属性是一个对象,这个对象就是通过调用构造函数创建的对象的原型。在原型对象上定义的变量或属性可以被所有对象实例共享。原来直接赋给对象实例的值,可以赋给他们的原型function Person(){}
Person.prototype.name = 'a';
Person.prototype.age = 29;
Person.prototype.job = 'engineer';
Person.prototype.sayName = function(){console.log(this.name);};
let person = new Person();
person.sayName();
我们可以把所有方法添加到原型上,这样就不会出现上面的创建一个对象就创建一个函数的情况了。
原型
只要创建一个函数,就会为这个函数创建一个prototype属性(指向原型)。所有原型会默认获得一个constructor
属性,指向关联的构造函数。
原型对象默认只会有constructor属性,其他属性都继承于Object, Object的原型是null。每次创建新实例时,内部的Post not found: Prototype特性就会指向原型,一般可以通过__proto__
访问这个原型。如果没有这个属性还可以使用getPrototypeOf()获得prototype。
原型层级
通过对象访问属性时,会按照属性的名称开始搜索。首先搜索对象实例本身,如果本身没有则会搜索原型,再搜索原型的原型,知道搜索到null。
虽然可以通过实例读取到原型对象上的值,但是不能修改这些值。如果在实例上创建了和原名对象中同名的属性,则会覆盖原型对象上的属性
function Person(){} |
如果想要恢复对原型属性的访问,则必须使用delete删除这个属性delete person1.name;
console.log(person1.name);//a
访问控制
我们可以使用hasOwnProperty()判断某个属性是实例上的还是原型上的,而in操作符只要可以访问就会返回trueconsole.log("name" in person2);//true
console.log(person2.hasOwnProperty("name");//false
如果使用for-in循环,那么可以通过对象访问且可枚举的属性都会返回,包括实例和原型属性。如果想要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法,这个方法只会返回该对象而不会返回原型的
function Person(){} |
如果我们想要获得所有实例属性,无论是否可以枚举,则可以使用Object.getOwnPropertyNames()let keys = Object.getOwnPropertyNames(person.prototype);
console.log(keys)//['constructor', 'name', 'age', 'job']
多了一个constructor,是不可枚举属性
除了getOwnPropertyNames()外,还有getOwnPropertySymbols()获得所有的symbol
for-in循环和keys的枚举顺序是不确定的。而getOwnProperty的顺序是先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。
对象迭代
可以使用Object.values()或Object.entries()获得对象的属性值,例如const o =
{
foo: 'bar';
baz: 1,
qux: {}
};
console.log(Object.values(o));//['bar', 1, {}]
console.log(Object.entries((o)));//{% post_link & %}
符号属性会被忽略
此外,我们还可以使用对象字面量对原型进行赋值function Person(){}
Person.prototype =
{
name: 'a',
age: 29,
job: 'engineer',
sayName()
{
console.log(this.name);
}
}
但是这样有一个问题,在创建Person时原型就已经确定了,这时原型指向的是Person。而此时原型又被重新创建,虽然Person.prototype指向没有问题,但是Person.prototype.constructor不指向Person,而是指向Object.我们可以通过在定义时指向Person来解决这个问题function Person(){}
Person.prototype = {
constructor: Person,//定义constructor
...
}
但是此时constructor变成了可枚举的了,如果想要把它变成不可枚举类型,可以使用defineProperty()
即使这样还可能出现问题
function Person(){}
let friend = new Person();
Person.prototype =
{
constructor: Person,
...
sayNmae()
{
console.log(this.name);
}
}
friend.sayName()//报错
报错的原因是虽然修改为新的原型,但是friend的prototype还指向老的原型。因此就没有sayName函数。
继承
原型链
js的继承是基于原型链的。例如function SuperType()
{
this.property = true;
}
SuperType.prototype.getSuperValue = function()
{
return this.property;
};
function SubType()
{
this.subproperty = false;
}
SubType.prototype = new SuperType();//继承
SubType.prototype.getSubValue = function()
{
return this.subproperty;
}
let instance = new SubType();
console.log(instance.getSuperValue());//true
上面的代码关键是SubType.prototype = new SuperType()
,通过创建一个父类的实例给子类的原型从而实现原型链的继承。现在子类的原型的原型就是SuperType.protoType
.
原型和实例的关系
有两种方式确定原型和实例的关系,一种是使用instanceof操作符,如果一个实例的原型链中出现过相应的构造函数。则instanceof返回trueconsole.log(instance instanceof Object);//true
console.log(instance instanceof SuperType);//true
console.log(instance instanceof SubType);//true
第二种方法时使用isPrototypeOf()
方法,这是给原型用的,只要原型链中包含这个原型就会返回trueconsole.log(Object.prototype.isPrototypeOf(instance);//true
console.log(SuperType.prototype.isPrototypeOf(instance);//true
但是原型链也有它的问题,例如function SuperType()
{
this.colors = {"red", "blue", "green"};
}
function SubType(){}
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);//red, blue, green, black
let instance2 = new SubType();
console.log(instance2.colors);//red, blue, green, black
它的问题就是所有子类的父类都是同一个,如果修改父类的属性那么对于所有子类都是有效的。也就是让本来不是原型属性的属性变成了原型属性。
另一个问题是,子类在实例化时不能给父类构造函数传参
盗用构造函数
为了解决原型链模式的一些问题,提出了一种盗用构造函数的方式。例如function SuperType()
{
this.colors = ['red', 'blue', 'green'];
}
function SubType()
{
SuperType.call(this);//盗用构造函数
}
let instance1 = new SubType();
instance1.colors.push('black');
console.log(isntance1.colors);//red, blue, green, black
let instance2 = new SubType();
console.log(instance2.colors);//red, blue, green
通过call()或apply()方法,这样相当于SuperType的构造函数在SubType中执行了,这样每个sub对象都有父类的实例属性。
call()方法是使用指定对象调用某个函数,第一个参数就是指定的对象,这里是this也就是SubType。也就是说SuperType中this.color变成了SubType.color,SubType上创建了color变量。
我们可以使用原型链和盗用构造方法结合的方式来进行继承
function SuperType(name) |
通过这两种方式的结合,我们在继承父类的方法和属性的同时也保存了隔离性
寄生式组合继承
原型式继承
例如:function objec(o)
{
function F(){}
F.prototype = o;
return new F();
}
其实就是最开始说的原型链,这里用一个函数封装一下,并且这种模式还可以使用Object.create()代替,它的第一个参数是用来做原型的对象,另外一个参数时给新对象定义额外属性的对象,例如
let person = |
寄生式组合继承
组合继承也存在效率问题,它的父类构造函数始终会调用两次,一次是创建子类原型时(prototype = new SuperType()),另一次是子类构造函数中(call调用)。
使用寄生式组合继承可以解决上面的问题function inheritPrototype(subType, superType)//使用这个代替原来的原型对象创建
{
let prototype = object(superType.protoType);//创建父类原型的副本
prototype.constructor = subType;
subType.prototype = prototype;
}
例如function SuperType(name)
{
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function()
{
console.log(this.name);
}
function SubType(name, age)
{
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);//用来替换SubType.prototype = new SuperType();
SubType.prototype.sayAge = function()
{
console.log(this.age);
}
类
定义
例如:class Foo
{
constructor(){}
get mybaz(){}
static myQux(){}
}
这里的类其实是将上面的继承封装起来的一种语法糖,本质上还是上面的一套东西。类中可以包含构造方法、实例方法、get和set函数和静态方法。
构造函数
可以使用new对对象进行实例化,实例化会进行如下操作:
- 在内存中创建一个新的对象
- 将新对象内部的Post not found: Prototype指针赋值为构造函数的prototype属性
- 构造函数内部的this指向该对象
- 执行构造函数内部代码
- 如果构造函数返回非空对象,则返回该对象。否则返回新创建的对象
如果构造函数没有定义,则默认会定义一个构造函数
类构造函数和普通构造函数的区别是,类构造函数必须要使用new,不使用会报错。而普通构造函数如果不使用new那么会将window作为this。
类中定义的constructor不会被当成构造函数,在对他使用instanceof操作符会返回falseclass Person
{
constructor(name, age)
{
this.name = name;
this.age = age;
}
}
let person = new Person('a', 29);
console.log(person instanceof Person);//true
console.log(person instanceof Person.constructor);//false
let person2 = new Person.constructor('b', 23);
console.log(person2 instanceof Person.constructor);//true
上例也就说明了类的原型指向这个类而不是指向构造函数
实例、原型和类成员
所有构造函数中的属性和方法都不是共享的class Person
{
constructor()
{
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
}
}
let p1 = new Person();
let p2 = new Person();
console.log(p1.name === p2.name);//false
console.log(p1.sayName === p2.sayName);//false
在类块中定义的其他方法实际上都是定义于原型上的,都是共享的class Person
{
constructor(){}
locate()
{
console.log("prototype");
}
}
let p = new Person();
Person.prototype.locate();//prototype
类方法等同于对象属性,因此可以使用可计算值作为键const symbolKey = Symbol("symbolKey");
class Person
{
stringKey()
{
console.log("invoked stringKey");
}
[symbolKey]()
{
console.log("invoked symbolKey");
}
}
let p = new Person();
p.stringKey();
p[symbolKey]();
在类外部添加成员数据
可以在类的外部手动添加属性,例如:class Person
{
sayName()
{
console.log(`${Person.greeting} ${this.name}`);
}
}
Person.greeting = 'My name is ';
Person.prototype.name = 'Jake';
p.sayName(); // My name is Jake
继承
可以使用extends关键字,继承任何拥有Post not found: Construct和原型的对象。这也就意味着它不仅可以继承类还可以继承构造函数。并且他会继承父类中所有属性和构造方法
class Vehicle{} |
可以使用super访问父类,可以使用super()调用父类的构造函数,但是使用时需要注意以下问题
- super只能在派生类构造函数和静态方法中使用
- 不能单独调用super,要么是用他的构造函数,要么使用它的构造方法
- 如果没有定义类构造函数,那么实例化子类时会调用super()
- 在构造函数中,不能再调用super()之前使用this
- 如果派生类中显示定义了构造函数,那么必须在其中调用super(),或者返回一个对象
例如:class Vehicle
{
constructor()
{
this.type = () =>{console.log(this);};
this.name = 'Vehicle';
}
}
class Bus extends Vehicle
{
constructor(){}
}
let b = new Bus();
b.type();
console.log(b.name);
//报错
抽象基类
虽然在js中并没有提供抽象类的关键字,但是我们可以手动实现
class Vehilce |
我们可以通过在构造函数中使用new关键字进行检测new的目标是否是抽象类来组织抽象类的实现。我们同样可以检测是否有某个函数定义来限制必须定义某个函数(接口)
类混入(继承多个类)
class Vehicle(){} |