了解对象
对象属性的类型
ECMA使用一些内部特性描述属性特征,不能直接访问,内部特性使用[[特性名]]标识
属性分为数据属性和访问器属性
数据属性
数据属性包含存储数据值得位置,从该位置读取值,写入值。
数据属性的内部特性:
[[Configurable]]表示属性是否可以通过delete删除并重新定义,默认true
[[Enumerable]]表示属性是否可以被for-in循环返回,默认true
[[Writable]]表示属性之是否可以被更改,默认true
[[Value]]包含属性值,也就是属性值读出和写入的位置
修改对象的默认属性需要使用Object.defineProperty(目标对象,属性名,描述符对象)
方法,例如:
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "ningzhi"
})
console.log(person.name);//ningzhi
person.name = "mike";
console.log(person.name);//ningzhi(修改未成功,writable属性未false)
//注意在use strict严格模式下修改只读属性会直接抛出错误
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
//defineProperty方法在调用时未作声名的属性都会默认被赋值false
访问器属性
访问器属性不包括数据值当然也就没有writable特性,他们包含getter()函数和setter()函数,读取访问器会触发getter方法,写入访问器会触发setter方法
[[Configurable]]表示属性是否可以通过delete删除并重新定义,默认true
[[Enumerable]]表示属性是否可以被for-in循环返回,默认true
[[Get]]获取函数,读取属性时调用,默认undefined
[[Set]]设置函数,设置属性时调用,默认undefined
let person = {
name_: "ningzhi",
sayName() {
console.log(this.name_);
}
}
Object.defineProperty(person,"name",{
get(){
return this.name_;
},
set(newName){
this.name_ = newName;
}
})
多个属性定义
ECMA提供了一个Object.defineProperties()
接受对象和值一次性以定义多个属性
let person = {}
// Object.defineProperties()
Object.defineProperties(person,{
name:{
value:"ningzhi"
},
age:{
value:20
}
//....
})
读取属性特性
Object.getOwnPropertyDescriptor()
方法可以取得制定属性描述符,
接收属性所在对象和属性两个参数返回包含特性的对象
ECMA2017新增了全局静态方法Object.getOwnPropertyDescriptors()
方法,该方法可以直接返回参数对象的所有特性
let descriptor = Object.getOwnPropertyDescriptor(person,"name");
console.log(descriptor.value);//ningzhi
console.log(descriptor.configurable);//false
//新方法
console.log(Object.getOwnPropertyDescriptors(person));
// age:
// configurable: false
// enumerable: false
// value: 20
// writable: false
// [[Prototype]]: Object
// name:
// configurable: false
// enumerable: false
// value: "ningzhi"
// writable: false
// [[Prototype]]: Object
// sayNmame:
// configurable: false
// enumerable: false
// get: ƒ()
// set: undefined
对象合并
ES6提供Object.assgin(目标对象,源1,源2,...)
方法接受一个目标对象和多个源对象作为参数,将源对象中的可枚举属性和自有属性复制给目标对象。其中是否枚举可使用Object.propertyIsEnumerable()
方法判断,自由属性使用Object.hasOwnProperty()
判断。注意:合并对象使用的浅复制只复制了对象的引用
,对象存在相同属性会以最后一次出现的属性值作为最终结果。对于错误的处理,复制一旦出错,操作会直接终止并退出,不可回滚。throw new error()
或者try{}catch(e){}
抛出错误
对象标识和相等判定
ES6以前===全等判定存在部分问题,例如:+0和-0判定为tru鹅。NaN判定必须使用isNaN()方法。ES6新增方法Object.is()方法解决了上述问题。
增强对象语法
便于处理对象
属性值简写
简写属性名只需要使用变量名不需要再赋值,前提是变量提前声明,如果没有声名就简写会抛出ReferencrError
错误
let name = "ningzhi";
let person = {
name,
age
};
console.log(person);//ReferenceError: age is not defined
可计算属性
可计算属性目标是能够动态的创建对象的属性键,可以根据已定义的常量生成而不是手动生成。使用[]包含未对象属性键使其以JavaScript表达式运行而不是字符串
const name = 'name';
const age = "age";
let person = {};
person[name] = "ningzhi";
person[age] = 20;
console.log(person);//{name:"ningzhi",age:20}
简写方法名
舍弃函数名:function(参数){}
写法使用函数名(参数){}
旧方式:
let person = {
sayName: function (name) {
console.log(`name是${name}`);
}
}
person.sayName("ningzhi");//name是ningzhi
新方式:
let person = {
sayName(name) {
console.log(`name是${name}`);
}
}
person.sayName("ningzhi");name是ningzhi
对象解构
使用与对象匹配的结构来实现对象属性赋值。
在不使用解构这一方法的时候如果我们要进行赋值操作的时候如果属性很多则会很繁琐,使用对象解构能优化代码
let person = {
name:"ningzhi",
age:20
};
let {name:uname,age:uage} = person;//普通写法
let {name,age} = person;//简写语法
console.log(uname+'---'+uage);//ningzhi---20
注意:
①解构时若解构对象有被解构对象中所没有的属性时需赋值否则未undefined
②解构与被解构对象可以不完全匹配
③null和undefined不能被解构
④解构的变量可以提前声明之后可以直接以({ })=object;
嵌套解构
解构的目标内部若存在嵌套不影响解构,但是注意存在嵌套时若在外层属性未定义的情况下不能使用嵌套解构,会找不到目标,目标对象没有的属性也不能解构undefined。外层属性:{内层属性:变量}
部分解构
涉及多个属性解构的操作是顺序化操作,会因为出错而中断。
参数上下文匹配
函数的参数列表arguments也可以进行解构赋值,可以将解构对象作为参数function 函数名(参数1,{解构对象},参数2...){}
创建对象
使用object构造函数和对象字面量创建对象创建具有相同接口的对个对象需要重复编码,性能存在优化空间。ES6开始支持类和继承,对原有结构进行了封装。
工厂模式(了解)
设计模式的一种,抽象创建特定对象的过程。创建函数来构造对象,一定程度上减少了代码的重复,使用函数加参数方式创建不同属性值的对象,但是存在问题无法标识对象(对象类型)
构造函数模式
ECMA的构造函数用于创建特定类型的对象。
构造函数与工厂函数区别:
①没有显式的创建对象
②属性和方法直接赋予this
③不用return
④构造函数名首字母大写区别于普通函数
⑤使用new操作符实例化
function Person(name,age){
this.name = name;
this.age = age;
}
let person1 = new Person("ningzhi",20);
console.log(person1);//Person{name:"ningzhi,age:20}
new操作符执行过程:
①内存中创建新对象
②新对象内部[[Prototype]]属性被赋予构造函数的prototype属性
③构造函数内部的this被赋值为这个新对象(this指向新对象)
④执行构造函数内部代码添加属性
⑤返回构造函数创建对象
[[prototype]]
的属性constructor
用于标识对象类型,一般指向的是构造函数本身。但是要表示对象类型更可靠的方式时使用instanceof
操作符
console.log(person1.constructor);//Person{}
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
所有的自定义函数都继承自Object
,这也是原型链会终止于Object,Object的原型为null。
构造函数两种声明方式:
function 构造函数名(参数){}
let 构造函数名 = function(参数){}
构造函数与普通函数
构造函数也是函数,两者唯一区别是调用方式不同。构造函数必须实例化
以后才能调用,而普通函数可以直接被调用。
当构造函数没有被实例化就被调用时,给定的方法和属性会被添加到window对象
下,也就是说当我们调用一个没有实例化并且没有使用call()/apply()
改变指向的构造函数时,他的属性和方法都会被添加到Global对象
。
要避免这种情况我们可以使用call()/apply()
将this的指向指定到特定的对象。
//构造函数指向问题
let person = new Person("ningzhi",20)
person.sayName();
//直接调用
Person("mike",21);
window.sayName();//mike,因为作用在全局对象即window对象上
//使用call()/apply()改变this指向
let o = {};
Person.call(o,"lili",20);
o.sayName();//lili,call将this指向定位到了o对象上
构造函数优化
对于函数内重复的方法,可以提取出来单独封装
成一个函数,在构造函数内调用即可,这样不会使得每一个构造函数的实例都去创建一个同名的方法。
*原型模式
每个函数都有prototype属性(对象),无论构造函数创建了多少个实例,他们的指向都是原型上的属性
// 原型模式
function Person() { }
//逐个赋值模式
Person.prototype.name = "ningzhi";
Person.prototype.age = 20;
Person.prototype.sayName = function(){
console.log(this.name);
}
//一次性赋值模式
// Person.prototype = {
// name:"mike",
// age:20,
// sayName(){
// console.log(this.name);
// }
// }
//测试
console.log(Person);
let person1 = new Person();
person1.sayName();//ningzhi
console.log(person1.name);//ningzhi
理解原型
创建函数必然会创建prototype属性(指向原型对象),原型对象也会获得一个constructor
属性指回构造函数。
自定义构造函数,他的prototype
只会获得constructor
属性,其他所有方法继承自Object
,如下:
function Person(){}
console.log(Person.prototype);
//{
//constructor:f Person(),
//[[prototype]]:Object
//}
创建构造函数的实例时,这个实例的[[prototype]]指针就会指向构造函数的[[prototype]],一般不可以访问,但是某些浏览器在对象上暴露了__proto__
属性,通过这个属性访问对象原型。
注意:实例与构造函数原型之间有联系,但是实例与构造函数本身没有联系
。
Object.getPrototypeOf()
可以获得对象的原型
修改原型继承关系Object.setPrototype(对象1,对象2)
,对象1的原型更改为2
但是这种方式可能影响代码性能,会涉及所有访问了修改过Prototype对象。
为避免性能下降,我们可以使用Object.create()
创建新对象并指定原型。
console.log(Object.getPrototypeOf(person1) == Person.prototype);
//true
let x = {
text:"测试"
}
//Object.create()以参数对象为原型创建实例
let person = Object.create(x);
person.age = 20;
console.log(person);
原型层级
通过对象访问属性时,按照属性名称搜索。首先搜索本对象,有则返回没有就访问对象原型,有则返回。
实例对象虽然可以访问原型属性但是不能对原型属性做更改
,如果对象原型上存在了一个属性,那我们在实例对象上再添加同名属性时,这个属性会遮蔽
对象上的属性,如果不用delete
删除属性,遮蔽就不会解除,将属性值置为null
也不能。
如何判断一个属性是否在对象原型上?
对象.hasOwnPrototy()
方法,当属性在实例上存在会返回true
,反之返回false
Object.getOwnPrototyDescriptor()
方法只能获得实例属性不能获得原型属性,要想获得原型属性,就直接在原型对象上调用Object.getOwnPrototyDescriptor()
function Person() { }
//逐个赋值模式
Person.prototype.name = "ningzhi";
Person.prototype.age = 20;
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person();
person1.text = "测试"
console.log(Object.getOwnPropertyDescriptor(person1,"name"));
//undefined
console.log(Object.getOwnPropertyDescriptor(person1,"text"));
//{value:"测试"...}
console.log(Object.getOwnPropertyDescriptor(Person.prototype,"name"));
//{value:"ningzhi"...}
原型和in操作符
in操作符可以单独使用也可以在for in中使用,确定属性是否在原型上。
(属性名 in 对象)
返回真假值判断是否在对象原型或者实例对象里。
但是这种方法不能判断属性是在实例上还是在原型上,所以Object.getOenProperty()
方法结合使用,当对象不在实例上且能找到就是在原型上。
function hasPrototyProperty(object,属性名){
return !Object.hasPrototyProperty(属性名) && (属性名 in object)
}
枚举属性五种方法:
for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
const element = object[key];
}
}
Object.keys();
Object.getOwnPropertyNames();
Object.getOwnPropertySymbols();
Object.assign();
前两者的枚举顺序受浏览器引擎影响,后三者的属性枚举顺序是有规律的。
总是先升序枚举数值键,再顺序枚举字符串(string)和符号键(symbol)。
对象迭代
两个对象迭代方法,前者返回的是包含对象值的数组,后者返回属性的键值对数组。非字符串会被转换为字符串,执行的是对象的浅复制,符号属性Symbol会被忽略。
Object.values();
Object.entries();
其他原型语法
可以在对象定义给属性定义时给constructor声名对象原型。但是这种方法的constructor属性是默认可以枚举的,我们得让其变为不可枚举。因此可以调用Object.defineProperty()
方法设置constructor的Enumerable属性为false
Object.defineProperty(构造函数.prototype,"constructor",{
enumerable:false,
value:构造函数
})
原型的动态性
从原型上搜索值的过程是动态的,当我们在修改原型之前已经存在实例的情况下,修改原型实例也会被修改。实例的prototype原型指针在复制以后就不会改变了,这个时候如果修改了构造函数的原型指向会导致构造函数和先前的实例原型指向不一,方法也不能共享(重写)。
原生对象原型
原生对象也有原型,并且可以在原型上给原生对象添加方法。不推荐添加,可能会引起冲突。
原型问题
弱化了向构造函数传递初始化参数的能力,实例都含有相同的初始属性及值。
继承
继承可以分为接口继承
和实现继承
,前者继承方法签名,后者继承实际的方法。由于JS没有方法签名,所以只支持实现继承,而实现继承依赖于原型链
。
原型链
ECMA将原型链定义为主要的继承方式。就像套娃一样,构造函数A拥有一个原型X,原型X有一个属性指回构造函数A,实例有一个指针指向构造函数A的原型X,而原型X有一个指针指向另一个原型Y,原型Y有一个指针指向构造函数B,以此类推,就可以形成一条链式结构
。
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());
//输出ture,虽然instance实例上没有直接定义getSuperValue方法,
//但是由于instance的原型SubType的原型SuperType有方法
//getSperType(),所以在第三次查找找到了该方法返回true。
默认原型
默认所有引用类型都继承自Object,也会获得Object的方法。这也有助于理解为什么所有的对象原型终止于Object,而Object的原型是null。
原型与继承关系
确定原型与实例的两种方法:
①instanceif操作符
②isPrototypeOf()方法
console.log(实例名 instanceof 原型名);
console.log(原型名.prototype.isPrototypeOf(实例名));
方法
当子类需要覆盖或者新增父类方法时,方法必须在原型上赋值再添加原型prototype上。
对象字面量方式创建原型方法会破坏原型链,导致父子关系破裂(重写)。
原型链问题
①原型中包含还能引用值时,引用值会被共享。
②子类型在实例化时不能给夫类型构造函数传参。
盗用构造函数
为解决问题①,出现了盗用构造函数方式也称对象伪装
或经典继承,针对属性问题。
原理是在子类构造函数中调用父类构造函数,使用apply和call以新创建的对象为上下文执行构造函数。缺点是构造函数中必须有方法并且方法无法复用。
组合继承
伪经典继承,使用原型链继承原型上的属性和方法,使用盗用函数继承实例的属性。
原型式继承
前文已经涉及到了Object.create()
,这一方法就是ECMA5对原型式继承的规范化。这个方法接受两个参数。作为新对象原型的对象和给新对象定义额外属性的对象(可选)。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
寄生式继承
寄生构造函数和工厂模式,创建一个实现继承的函数,增强对象,在返回对象
function createAnother(original) {
let clone = object(original);//调用函数创建新对象
clone.sayHai = function () {//增强对象
console.log("hi");
}
return clone;
}
寄生式组合继承
不调用父类构造函数给子类原型赋值,取得父类副本,使用寄生式继承父类原型,将返回的新对象赋值给子类原型。
function inheriPrototype(subType,superType){
let prototype = object(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype;//赋值对象
}
类
类定义
类声明和类表达式,与函数类似但是类声明不会被提升。且函数受函数作用域限制,类受块作用域限制。
class Person{}
let Person = class {};
类的构成:构造函数方法、实例方法、获取函数、设置函数、静态类方法
都是可选参数,空类定义可以生效class zoo { }
类构造函数
constructor
关键字可以在类定义块内部创建类的构造函数,不定义构造器函数将会被视作空函数
实例化类
使用new实例化类时相当于使用new调用它的constructor构造函数。
new执行操作:
①在内存中创建新对象
②新对象内部[[prototype]]指向构造函数的prototype
③构造函数内部的this被设置指向这个新对象
④执行构造函数内部代码
⑤构造函数返回非空对象,否则返回新对象
类构造函数必须使用new调用,没有会报错。
类实际上就是特殊的函数。
class Test{}
class Person{
constructor(){
console.log("person");
}
}
class Zoo {
constructor(){
this.color = "black"
}
}
let a = Test();
//Uncaught TypeError:
///Class constructor Test cannot be invoked without 'new'
let b = new Person();//构造函数执行以后会返回this
let c = new Zoo();
console.log(c.color); //black
console.log(typeof Person);//function
实例、原型、类成员
实例成员
调用new就会执行构造函数创建新实例,每一个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享。
原型方法、访问器
类定义语法把在类块中定义的方法视为原型方法。
添加到this上的方法只会视作实例方法,直接在类块中定义的方法才会在类的原型上。
原型方法可以定义在类块里也可以定义在构造函数里。
类定义还支持设置获取访问器。
类方法等同于对象属性,可以使用字符串、符号Symbol、计算值[ ]。
类静态方法
类静态方法常用于执行不特定于实例的方法(共用方法),也不需要类实例的存在。
类静态成员在类定义中使用static关键字做前缀,在静态成员中,this引用类自身。
class Person {
constructor() {
//this作用于不同实例
this.locate = () => console.log("instance", this);
}
//定义在原型对象上
locate() {
console.log("prototype", this);
}
//定义在类本身
static locate() {
console.log("class", this);
}
}
let p = new Person();
p.locate();//实例的this
Person.prototype.locate();//类的原型的constructor
Person.locate();//类自身
继承
继承基础
ES6支持单继承,使用extends关键字,可以继承任何拥有[[constructor]]的原型和对象。构造函数也可以被继承。
class 新类 extends 被继承类 {};
派生类可以通过super关键字引用原型。此关键字仅限派生类使用,且只能在构造函数,实例方法和静态方法中使用。
class Person {
constructor(){
this.hasEngine = true;
}
}
class Another extends Person{
constructor(){
console.log(this instanceof Person);//true
//super必须在this之前使用
// 否则报错Must call super constructor in derived class before accessing
// 'this' or returning from derived constructor at new Another
super();//等价于super.constructor()
console.log(this instanceof Person);//true
console.log(this);//Another {hasEngine:true}
}
}
new Another();
super使用注意事项:
①super只能在派生类构造函数和静态方法中使用
②不能单独使用super关键字,要调用构造函数或者引用静态方法
③调用super()会调用父类构造函数并将返回的实例赋值给this
④super()行为如同调用构造函数,如果要给父类传参需要手动传入
⑥如果没有定义类构造函数,实例化派生类时会调用super()并传入参数
⑦在类构造函数中this调用不能先于super()
⑧若派生类中显示定义构造函数,要么调用super()要么返回一个对象
抽象基类
可以被其它类继承但是本身不会实例化的类。可以使用new.arget
实现,也可用于检测是否抽象基类。
代码地址:Gitee