3w字长文带你从JS的对象一路通关到类
ECMA-262将对象定义为:一组属性的无序集合。 我们可以把JS中的对象想象成一张散列表,其中的内容就是一组键值对,值的类型可以是数据或者函数。
一、理解对象
创建自定义对象的通常方式是 new
一个 Object
的新实例,然后再给这个实例添加属性和方法。
let person = new Object();
person.name = 'Macc';
person.age = 18;
person.sayHi = function(){console.log('hi');
}
⭐现在更流行的是对象字面量方式定义对象
//对象字面量
let person = {name:'Macc',age:16,sayHi:function(){console.log('hi');}
}
上面两个对象的属性和方法都一样,可视为两者是等价的(注意,是等价而不是相同或者说是同一个对象)。
对象的属性都有自己的特征,而这些特征决定了它们在JS中的行为。
(一)属性的类型
ECMA-262用一些内部特性来描述属性的特征。既然是内部特性,也就是说开发者在JS中是不能直接访问这些特性的。
规范使用两个中括号括住特性的名称,以此来标识其为内部特性。例如:[[Enumberable]]
对象的属性分为两种:数据属性和访问器属性。
1.数据属性
数据属性有4个特性描述其行为
特性名称 | 作用 | 默认值 |
---|---|---|
[[Configurable]] | 表示属性是否可通过 delete 删除并重新定义,是否可以修改属性的特性,是否可以把属性类型改为访问器属性。 | true |
[[Enumberable]] | 属性是否可以通过 for-in 循环返回。 | true |
[[Writable]] | 属性的值是否可以被修改。 | true |
[[value]] | 属性实际值的存放位置,读取和写入属性值的都是操作这个位置。 | undefined |
修改默认(内部)特性
上面提到过,开发者在js中是无法直接访问内部特性的。所以要修改属性的内部特性,必须要使用 Object.defineProperty()
方法。
Object.defineProperty(obj,‘propertyName’,decriptionObj)方法
参数名 | 参数类型 | 描述 |
---|---|---|
obj | Object | 要添加/修改属性的对象 |
propertyName | String | 要添加或修改的属性的名称 |
decriptionObj | Object | 描述符对象 |
描述符对象上的属性可以包含4个内部特性名称(即以内部特性的名称为属性名)。
let person = {};//定义一个person对象
//给对象添加属性
Object.defineProperty(person,'name',{writable:false, //属性值不可修改value:'Macc' //属性的实际值
});
console.log(person.name);//访问属性值,输出'Macc'
person.name = 'GaGa';//尝试修改属性的值
console.log(person.name);//输出'Macc'
因为person
对象的name
属性的内部特性writable
被改为false
了,表示属性的值不可以被修改,因此后面尝试将其改为GaGa的时候,修改行为被忽略(在严格模式下会报错),依旧输出原值。
若把属性的configurable
属性设为false
,则不能再改回true
,此时再调用Object.defineProperty()
方法并修改任何非writable
属性都会报错。
调用Object.defineProperty()
时,configurable、enumerable、writable
的值,若不指定,默认为false
。
let person = {name: 'Macc',age: 18
}
Object.defineProperty(person, 'hair', {value: 'black',//未指定其他值,其他值默认为false
});let _p = Object.getOwnPropertyDescriptor(person, 'hair');
console.log(_p);
2.访问器属性
访问器属性不包含数据值。
访问器属性包含一个获取函数getter和一个设置函数setter。
- 在读取访问器属性时,会调用
getter
,getter
的责任是return
一个有效值。 - 在写入访问器属性时,会调用
setter
并传入新值,setter
决定对数据(属性的值)做什么修改。
访问器属性也有4个特性描述其行为
特性名称 | 作用 | 默认值 |
---|---|---|
[[Configurable]] | 表示属性是否可通过 delete 删除并重新定义,是否可以修改属性的特性,是否可以把属性类型改为数据属性。 | true |
[[Enumberable]] | 属性是否可以通过 for-in 循环返回。 | true |
[[Get]] | getter,在读取属性时调用。 | undefined |
[[Set]] | setter,在写入属性时调用。 | undefined |
⭐修改访问器属性也要使用Object.defineProperty()
方法。
let book = {year_: 2017,//私有成员edition: 1 //公共成员
}
Object.defineProperty(book, 'year', {get() {return this.year_;},set(newValue) {if (newValue > 2017) {this.year_ = newValue;this.edition = newValue - 2017;}}
});book.year = 2018;
console.log(book.edition); //输出2
上面代码就是访问器属性的典型使用场景:设置一个属性的值会导致一些其他变化的发生。
getter
和 setter
不一定都要定义:
- 只定义
getter
意味着属性是只读的,尝试修改属性会被忽略; - 只定义
setter
在非严格模式下读取属性会返回undefined
。
ES5之前没有 Object.defineProperty()
方法。
3.同时定义多个属性
使用 Object.defineProperties(obj,descriptionObj)
方法
参数名 | 参数类型 | 描述 |
---|---|---|
obj | Object | 要添加/修改属性的对象 |
decriptionObj | Object | 描述符对象 |
let person = {}
Object.defineProperties(person, {name: {value: 'Macc'},age: {value: 18},hair: {get() {return 'black';},set(newValue) {//....}}
});
4.读取属性的特性
4.1 读取属性的某个特性
使用方法 Object.getOwnPropertyDescriptor(属性所在对象,属性名称)
,该方法 return
一个对象。
4.2 读取对象的全部自有属性的特性
使用方法 Object.getOwnPropertyDescriptors(对象)
,该方法也 return
一个对象,对象包括指定对象的所有自有属性的特性,若对象没有属性,则返回一个空对象。
该方法实际上是在每个自有属性上调用 Object.getOwnPorpertyDescriptor()
方法并在一个新对象中返回它们。
(二)合并对象
把源对象的所欲本地属性一起复制到目标对象上,这种操作叫做合并(merge),也叫做混入(mixin)。
合并对象使用的方法是 Object.assign(目标对象,源对象1, ... ,源对象n)
。
该方法会将源对象中的所有可枚举属性和自有属性复制到目标对象上。
所谓的可枚举属性指的是调用Object.prpertyIsEnumerable()
返回true
的属性;
所谓的自有属性指的是调用Object.hasOwnProperty()
返回true
的属性;
复制过程中,会使用源对象上的 [[Get]]
取得属性值,再使用目标对象上的 [[Set]]
设置属性的值。
Object.assign()
执行的是浅复制,只复制对象的引用。
若多个源对象有相同属性,则使用最后一个复制的值(即哪个源对象靠后就使用哪个的值)(覆盖)
不能在两个对象之间转移getter函数和setter函数。 从源对象访问器属性中取得的值比如getter函数,会作为一个静态的值赋值给目标对象。
若是在赋值期间出错,则操作中止并退出,抛错。但是该方法不会回滚,它是一个尽力而为,可能只完成部分复制的方法。
(三)对象标识及相等判定
在ES6之前,存在使用全等符(===
)也无能为力的情况:
- 符合预期的情况如下
表达式 | 结果 |
---|---|
true === 1 | false |
{} === {} | false |
"2" === 2 | false |
2.不同js引擎表现不同,但仍被认为相等
表达式 | 结果 |
---|---|
+0 === -0 | true |
+0 === 0 | true |
-0 === 0 | true |
- 要确定
NaN
的相等性必须使用isNaN
函数
表达式 | 结果 |
---|---|
NaN === NaN | false |
isNaN(NaN) | true |
在ES6中新增了方法 Object.is()
,该方法与全等符相似,但是考虑了上述的边界条件。该方法必须接收两个参数。
表达式 | 结果 |
---|---|
Object.is(+0,-0) | false |
Object.is(+0,0) | true |
Object.is(-0,0) | false |
Object.is(NaN,NaN) | true |
如果要使用 Object.is()
检查超多两个值,可以递归的利用相等性实现:
function recursivelyCheckEqual(x, ...rest) {return Object.is(x, rest[0]) &&(rest.length < 2 || recursivelyCheckEqual(...rest));
}console.log(recursivelyCheckEqual(1, 2, 3, 4)); //false
(四)增强的对象语法(语法糖)
1.属性值的简写
简写属性值只要使用变量名就会自动被解释为同名属性键,若是未找到同名的变量,则抛错。
let name = 'Macc',age = 18;
let person = {name, //简写//下面是以前的写法age:age
};
console.log(person);//{name:"Macc",age:18}
2.可计算属性
在引入可计算属性前,如果想使用变量值作为属性(名),必须先声明对象,再使用中括号语法来添加属性。也即是说,不可以在对象字面量中直接动态命名属性。
const nameKey = 'name';
let person = {};//先声明对象
person[nameKey] = 'Macc';//使用中括号语法添加属性
在引入可计算属性后,就可以在对象字面量中完成动态属性赋值了。
let person = {[nameKey]:'Macc'
}
可计算属性表达式中抛出任何错误都会中断对象的创建,且不回滚。
const nameKey = 'Macc';
const ageKey = 'age';let person = {[nameKey]: 'Macc',[jobKey]: '码农', //这里会出错[ageKey]: 18
}
console.log(person);//这里是打印不出来的,因为对象的创建被中断了。
3.简写方法名
在此之前,给对象定义方法的时候,是以下格式:
let person = {//方法名 冒号 匿名函数表达式sayHi:function(){//...}
}
现在则是:
let person = {sayHi(){//...}
}
而且,简写方法名与可计算属性相互兼容。
const methodKey = 'sayHi';
let person = {[methodKey](name){//...}
}
(五)对象解构
在一条语句中使用嵌套数据实现一个或多个赋值操作。
简而言之就是,使用与对象 匹配的结构来实现对象属性的赋值。
匹配的结构:就有点对号入座的味道。
- 可以使用简写语法
let person = {name:'Macc',job:'码农'
};
let {name,job} = person;
console.log(name,job);//Macc,码农
- 解构赋值不一定与对象的属性匹配(赋值时可以忽略某些属性,无需一一对应)
let {name,age} = person;//无需一一对应
console.log(name,age);//"Macc",undefined
- 可在解构赋值的同时设定默认值
let {name,age:18} = person;
console.log(name,age);//"Macc",18
解构在内部使用了 ToObject()
方法把源数据解构转换为对象,也就是说在对象解构上下文中,原始值会被当成对象。null和undefined不能被解构,否则报错。
let { _ } = null;//报错
let { _ } = undefined;//报错
解构不要求变量必须在解构表达式中声明,但是如果给事先声明过的变量赋值,则表达式必须包在一对小括号中。
let personName,personAge;//事先声明变量
({name:personName,age:personAge} = person);
1.嵌套解构
首先是可以使用解构来复制对象的属性:
let person = {name: 'Macc',age: 18,
};let personCopy = {}; //这里这个分号一定要记得加,不然报错({ name: personCopy.name, age: personCopy.age } = person);
console.log(personCopy);
//{name: 'Macc', age: 18}
然后想一想,假如被复制的属性是个嵌套结构呢?解构还能用吗?答案是可以的,解构赋值可以使用嵌套结构,但是外层属性未定义时,不能使用。
let person = {job: {title: '码农'}
};let personCopy = {}; //这里这个分号一定要记得加,不然报错let { job: { title } } = person;
console.log(title); //码农//foo在源对象上undefined未定义,报错
({ foo: { bar: person.bar } } = person);
//job在源对象上undefined未定义,报错
({ job: { title: person.job.title } } = person);
2.部分解构
如果一个解构表达式设计多个赋值操作,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分。
3.参数上下文匹配
在函数参数列表中也可以进行解构赋值,且不会影响到 arguments
对象。
let person = {name: 'Macc',age: 18
};function printPerson(foo, { name, age }, bar) {console.log(arguments);
}
printPerson('1st', person, '2nd');
二、创建对象
使用Object构造函数和对象字面量的方式创建对象的不足之处:
创建具有同样接口的多个对象需要重复编写很多代码。
let person1 = {name:'Macc',age:18,sayHi(){console.log('hi');}
};let person2 = {name:'Deing',age:16,sayHi(){console.log('hi');}
}
(一)工厂模式
这是一种设计模式,用于抽象创建特定对象的过程。这个模式后面再详细说它。
function PersonFactory(){//创建一个新对象let o = new Object();//给新对象添加大家共有的属性和方法o.sayHi = function (){console.log('hi');}//return 这个对象return o;
}
let person1 = PersonFactory();
let person2 = PersonFactory();person1.sayHi();//'hi'
person2.sayHi();//'hi'
(二)构造函数模式
JS中的构造函数是用于创建特定类型对象的。比如我希望得到一个Person类型的对象person1,那么就是用Person构造函数。
构造函数模式的特点:
- 没有显式的创建对象
- 属性和方法直接赋值给
this
- 没有
return
按照惯例,构造函数的名称的首字母要大写。
//Person类型构造函数
function Person(){this.sayHi = function(){console.log('hi');}
}
//创建Person类型的实例对象
let p1 = new Person();
let p2 = new Person; //无需传参可以省略括号,new操作符则必不可少。
创建实例要使用 new
操作符,使用 new
调用构造函数会执行如下操作:
- 在内存中创建一个新对象;
- 新对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性; - 构造函数内部的
this
被赋值为这个对象 (改变this指向); - 执行构造函数内部代码 (给新对象添加属性、方法);
- 若构造函数返回非空对象,则返回该非空对象,否则返回刚创建的新对象。
上面的代码中,p1和p2分别保存着Person的不同实例。p1和p2都有一个 constructor
属性指向Person。
console.log(p1.constructor == Person); //true
constructor
属性是用来标识对象类型的,但是一般认为 instanceof
操作符更可靠。
console.log(p1 instanceof Person);//true
console.log(p1 instanceof Object);//true
所有的自定义对象都是Object的实例,因为所有自定义对象都继承自Object。
构造函数也可以写成函数表达式的形式:
let Person = function(){this.sayHi = function(){console.log('hi');}
}
1.构造函数也是函数
构造函数和普通函数的唯一区别就是调用的方式不同。
任何函数只要使用 new
操作符调用就是构造函数。
⭐ 在调用一个函数而没有明确设置this值得情况下(即:没有作为对象的方法被调用或者未使用 call()\apply()
调用),this始终指向 global
对象。
//这里是全局作用域
var name = 'Macc';
function sayHi(){console.log('hi,' + this.name);
}
//调用sayHi函数,注意,此处的sayHi函数是直接调用的,没有作为某个对象的方法被调用(即:没有通过 xxx.sayHi() 这种方式被调用)
sayHi();// 'hi,Macc'
2.构造函数模式的弊端
构造函数的主要问题在于:其定义的方法会在每个实例上都创建一遍。 因此,不同实例上的函数虽然同名但是不相等。
function Person() {this.hair = 'black';this.sayHi = function() {console.log('hi');}
}let p1 = new Person();
let p2 = new Person();console.log(p1.sayHi === p2.sayHi);//false
但是正常来说,同名函数做的事情是一样的,所以没必要定义两个不同的Function实例。
就像小明和小红(两个实例),他俩的钥匙都丢了,去找开锁师傅,正常来说只需要一个开锁师傅就足够了,没必要给小明专门培养一个开锁师傅,给小红专门培养一个开锁师傅。
要解决这个问题,其中一种思路是:把函数的定义转移到构造函数外部。 然后实例中的方法属性只包含一个指向外部函数的指针,所以实例共享了定义在外部(一般是全局作用域)上的函数。
function Person() {this.hair = 'black';this.sayHi = sayHi;
}function sayHi() {console.log('hi');
}let p1 = new Person();
let p2 = new Person();console.log(p1.sayHi === p2.sayHi);//true
上面的这种思路,虽然解决了相同逻辑的函数重复定义的问题,但是却污染了全局作用域。 因此,我们可以引入原型模式来更好的解决这个问题。
(三)原型模式
每个函数都会创建一个 prototype
属性,该属性是一个对象。该对象包含了由特定引用类型的实例共享的属性和方法。
我们称该对象( prototype
属性)为我们通过调用构造函数创建的对象(实例)的原型。
在原型对象上定义的属性和方法被对象的实例所共享。
1.理解原型
(1)只要创建函数,就会为该函数创建一个 prototype
属性,该属性指向原型对象;
(2)默认情况下,所有的原型对象会自动获得一个 constructor
属性,该属性指回与之关联的构造函数;
(3)在自定义构造函数时,原型对象默认只会获得 constructor
属性,其他所有方法均继承自Object;
//创建一个函数
function Person() {}
/*就会为该函数创建一个prototype属性,*该属性是个对象,我们称这个属性为原型,这个对象为原型对象*/
console.log(Person.prototype); //{constructor: ƒ}
//默认情况下,原型对象自动获取一个constructor属性
//该属性指回与之关联的构造函数
console.log(Person.prototype.constructor === Person); //true
(4)每次调用构造函数创建一个新的实例,实例内部的 [[Prototype]]
指针会被赋值为构造函数的原型对象。
JS中没有访问 [[Prototype]]
特性的标准方式,但是Firefox、Safari、Chrome会在每个对象上暴露 __proto__
属性,可以通过该属性来访问对象的原型。
同一构造函数创建的不同实例,共享同一个原型对象。
//创建一个构造函数
function Person() {this.hair = 'black';
}let p1 = new Person(); //创建实例
let p2 = new Person();//实例内部的[[pPrototype]]指针被赋值为构造函数的原型对象
console.log(p1.__proto__ === Person.prototype); //trueconsole.log(p1 === p2); //false 说明是不同的实例
//同一构造函数创建的不同实例共享同一个原型
console.log(p1.__proto__ === p2.__proto__); //true
(5)正常的原型链都终止于Object的原型对象,而Object的原型的原型是null。
(6)核心需要理解的一点是:实例和构造函数原型之间有直接关系,但是实例与构造函数之间没有。
这句话我没有理解,实例不是通过调用构造函数产生的吗?为什么说实例与构造函数之间没有关系呢。
想一下前面说到的,通过new操作符调用构造函数的时候执行了什么操作,就理解了。
(7)可以使用 isPrototypeOf()
方法确定两个对象之间的关系(实例和构造函数的关系吧)。
本质上,该方法会在传入参数的 [[Prototype]]
指针指向调用它的对象时返回 true
。
//构造函数
function Person() {}
//创建实例
let p1 = new Person;
//使用isPrototypeOf方法
console.log(Person.prototype.isPrototypeOf(p1)); //true
⭐(8)JS中Object有一个方法叫做 Object.getPrototypeOf()
会返回参数内部特性 [[Prototype]]
的值。
console.log(Object.getPrototypeOf(p1) === Person.prototype); //true
⭐(9)get和set经常成对出现,所以还有个方法叫 Object.setPrototypeOf()
,该方法可以向实例的私有特性 [[Prototype]]
写入一个新的值。这样就就重写了一个对象的原型链关系。
let biped = {numLegs: 2
};
let person = {name: 'Macc'
};
Object.setPrototypeOf(person, biped);
console.log(person);
console.log(person.numLegs);
上面的截图可以看出,person对象是没有numLegs属性的,但是通过Object.setPrototypeOf()
方法改变了person的原型对象为biped对象,然后顺着原型链,找到了numLegs属性,输出为2。
这个方法可能会严重影响代码的性能, 而且它造成的影响是很深层次的,所以一般不建议使用。
⭐(10)有问题就要解决问题,为了避免使用 Object.setPrototypeOf()
可能造成的性能下降,可以通过使用 Object.create()
方法创建新对象并为其指定原型对象。
let biped = {numLegs: 2
};
let person = Object.create(biped)
console.log(person); //{}
console.log(person.numLegs); //2
2.原型层级
这部分字比较多,但是挺好理解的。
原型用于在多个对象实例间共享属性和方法的原理: 在通过对象访问属性时,会按照这个属性的名称开始搜索。
- 搜索开始于对象实例本身,若在自身发现了对应属性,则返回对应属性的值,停止搜索;
- 若未发现,则搜索会沿着
[[Prototype]]
指针进入原型对象中搜索,找到则返回值;
上面的过程我们可以得知,我们可以通过实例读取原型对象上的值,但是另外一点就是,我们不能通过实例修改/重写这些值。( 可读不可写 )
如果在实例上添加了一个与原型对象中同名的属性,则会在实例上创建这个属性,这个属性会遮蔽住原型对象上的属性。(即:只要给对象实例添加一个属性,这个属性就会遮蔽shadow原型对象上的同名属性。虽然不会修改它,但是会屏蔽对它的访问。 )
即使把实例上的这个属性设置为 null
,也恢复不了它和原型的联系,但是,使用 delete
操作符可以完全删除实例上的这个属性,进而恢复搜索过程。
2.1 hasOwnProperty()
用于确定某个属性时在实例上还是在原型对象上,该方法继承自Object,会在属性存在于调用它的对象实例上时返回 true
。
function Person(){}
Person.prototype.name = 'Macc';let p1 = new Person();
p1.age = 18;p1.hasOwnProperty('name');//false
p1.hasOwnProperty('age');//true
这里补充一点前面的知识点:
Object.getOwnPropertyDescriptor()
方法只对实例属性生效。 要取得原型属性的描述符,必须直接在原型对象上调用该方法。
3.原型和in操作符
in
操作符有两种使用方式:单独使用和在 for - in
循环中使用。
3.1 单独使用
单独使用时,in
会在可以通过对象访问指定属性时返回 true
,无论该属性是在实例上还是原型上。
let person = {name:'Macc'
};console.log('name' in person);//true
像下面这样,同时使用 hasOwnProperty()
和 in
操作符可以确定某个属性是否存在于原型上。
function hasPrototypeProperty(obj,name){return !obj.hasOwnProperty(name) && (name in obj);
}
//只要 hasOwnProperty 返回 false;
//in 返回 true
//说明该属性是个原型属性
不是很明白这个函数的意义是啥,单独使用
hasOwnProperty()
不是就已经区分出该属性是实例属性还是原型属性了吗?
3.2 for-in循环
在 for-in
循环中使用时,可通过对象访问 且 可被枚举的属性都会返回,包括实例属性和原型属性。
遮蔽原型中不可枚举属性([[Enumerable]]特性为false
)的实例属性也会返回,因为默认情况下,开发者自定义的属性都是可枚举属性。
(1)要获得对象上所有可枚举实例属性,使用 Object.keys(obj)
方法。
- 该方法接收一个对象做参数;
- 返回一个数组, 包含对象所有可枚举属性名称的字符串数组。
(2)要列出所有实例属性,无论是否可以枚举,使用 Object.getOwnPropertyNames(obj)
。
不可枚举属性:constructor
属性
(3)ES6新增Symbol类型后,因为以Symbol为键的属性没有名称的概念,因此,Object.getOwnPropertySymbols()
方法就出现了,该方法只针对Symbol。
let k1 = Symbol('k1'),k2 = Symbol('k2');let o = {//这里就用到了前面说的可计算属性[k1]: 'k1',[k2]: 'k2',
}console.log(Object.getOwnPropertySymbols(o)); //[Symbol(k1), Symbol(k2)]
4.属性枚举顺序
(1)for-in
循环、Object.keys()
的枚举顺序是不确定的;
(2)Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Object.assign()
的枚举顺序是确定的。
- 先以升序枚举数值键;
- 然后以插入顺序枚举字符串和符号键;
(3)对象字面量中定义的键以它们的逗号分隔的顺序插入;
这个枚举顺序咋体现?不是遍历的先后顺序吗?我用for-in和keys方法也是确定的呀,先数值键再插入顺序,为啥?
(四)对象迭代
ES6新增两个静态方法,用于将对象内容转换为序列化的格式。 这两个方法是 Object.values(obj)
和 Object.entries(obj)
。两者都接收一个对象做参数,返回对象内容的 数组 。
Object.values(obj)
返回的是 对象值 的(一维)数组;Object.entries(obj)
返回的是键值对 的(二维)数组;
const o = {foo:'bar',qux:{},baz:1,
};console.log(Object.values(o)); //['bar',{},1]
console.log(Object.entries(o));
//[['foo','bar'],['qux',{}],['baz',1]];
(1)这两个方法执行的都是浅复制;
const o = {foo: 'bar',obj: {name: 'Macc'}
};let o1 = Object.values(o);
let o2 = Object.entries(o);console.log(o1[1] === o2[1][1]); //true 浅复制
(2)非字符串属性会被转换为字符串输出;
(3)符号属性会被忽略;
const sym = Symbol();
const o = {[sym]: 'foo',
};console.log(Object.values(o)); //[]
console.log(Object.entries(o)); //[]
1.其他原型语法
给原型对象添加属性和方法有2种方式。
1.1 直接添加
function Person() {}Person.prototype.name = 'Macc';
Person.prototype.sayHi = function() {console.log('hi');
}
1.2 重写原型
直接通过一个包含所有属性和方法的对象字面量来重写原型。
function Person() {}Person.prototype = {name: 'Macc',sayHi: function() {console.log('hi');}
}
在这种写法中,Person.prototype
被设置为一个新对象,这样的作法有一个问题:Person.prototype
的 constructor
属性不再指向构造函数 Person
了。
console.log(Person.prototype.constructor === Person); //false
因为这种写法,完全重写了默认的 prototype
对象,因此其 constructor
属性也指向了完全不同的新对象(Object构造函数)。
虽然使用 instanceof
操作符依然能返回可靠的值,但是却不能再依靠 constructor
属性来识别类型了。
如果 constructor
属性的值很重要,则我们可以在对象字面量中专门设置一下它的值。
function Person() {}Person.prototype = {constructor: Person, //专门设置一下,但是这样也有问题name: 'Macc',sayHi: function() {console.log('hi');}
}
用上面的这种方法虽然恢复了 constructor
属性的问题,但是这样仍然有一个问题,那就是现在恢复后的 constructor
属性的枚举特性 [[Enumerable]]
是 true
。
但是原生的 constructor
属性是不可枚举的,因此我们更可能会在重写原型对象后,再使用 Object.defineProperty()
方法恢复 constructor
属性。
function Person() {}Person.prototype = {name: 'Macc',sayHi: function() {console.log('hi');}
}Object.defineProperty(Person.prototype, 'constructor', {enumerable: false,value: Person
})console.log(Person.prototype.constructor === Person); //true
2.原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前就已经存在了,任何时候对原型对象所做的修改也会在实例上反映出来。
function Person() {}
//在修改原型前创建一个实例
let p1 = new Person();
//修改原型
Person.prototype.sayHi = function() {console.log('hi');
};
//修改会在实例上反映出来
p1.sayHi(); //hi
虽然随时可以给原型添加属性和方法,并且立即能在所有实例上反映出来,但是修改原型对象和重写原型对象这是两码事。
重写整个原型会切断最初原型与构造函数的联系(因为 constructor
属性变了),但是实例仍然引用的是最初的原型,而最初的原型上不一定有我们定义的方法,进而会引发报错。
function Person() {}
//在修改原型前创建一个实例
let p1 = new Person();
//重写原型
Person.prototype = {name: 'Macc',sayHi: function() {console.log('hi');}
};
//这里就会报错
p1.sayHi();
实例只有指向原型的指针,而没有指向构造函数的指针。
重写原型之后再创建的实例才会引用新的原型。
function Person() {}
//在修改原型前创建一个实例
let p1 = new Person();
//重写原型
Person.prototype = {name: 'Macc',sayHi: function() {console.log('hi');}
};
//重写后创建的p2
let p2 = new Person();
p2.sayHi();//'hi'
3.原生对象的原型
所有原生引用类型的构造函数(Array、Object、String等)都在原型上定义了实例方法,通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。
但是不推荐这么做:
- 可能造成命名冲突(因为各浏览器的不同);
- 可能意外的重写原生的方法;
推荐的做法是:创建一个自定义的类,然后继承原生类型。
4.原型的问题(弊端)
(1) 弱化了向构造函数传初始参数的能力,导致所有实例默认都取得相同的属性值;
(2)原型的最主要问题源于它的共享特性。主要是共享引用值的属性。
function Person() {}Person.prototype = {arrName: ['Macc', 'Deing']
}let p1 = new Person();
let p2 = new Person();p1.arrName.push('Gaga'); //修改实例p1的属性,会在p2上反映出来
console.log(p2.arrName); //['Macc', 'Deing', 'Gaga']
但是一般来说,不同的实例应该有属于自己的属性副本。
三、继承
很多面向对象的语言都支持两种继承:接口继承、实现继承。
接口继承只继承方法签名,这种继承在ECMAScript中是不可能的,因为函数没有签名。
实现继承继承实际的方法,这种继承是ECMAScript唯一支持的继承方式,主要通过原型链实现。
单看定义可能有点懵,可以对比下面的JS和Java的代码来意会:
(一)原型链
ECMA-262把原型链定义为ECMAScript的主要继承方式。
其基本思想就是:通过原型链继承多个引用类型的属性和方法。
1.原型链的基本构想
这里要从构造函数、原型、实例三者的关系出发,每个构造函数都有一个原型对象(prototype
),原型有一个属性(constructor
)指回构造函数。而实例则有一个内部指针([[Prototype]],__proto__
)指向构造函数的原型。
那么,如果原型是另一个类的实例呢? 这就会意味着这个原型本身就有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数的原型。
这样就在实例和原型之间构造了一条原型链。
2.默认原型
默认情况下,所有引用各类型都继承自Object。 这也是通过原型链实现的,任何函数的默认原型都是一个Object实例。
3.原型与继承的关系
原型与实例之间的关系有2种确定方法
3.1 instance
操作符
如果一个实例的原型链中出现过相应的构造函数,则 instance
操作符就会返回 true。
//父类构造函数
function Father() {}
//子类构造函数
function Child() {}
//子类继承父类
Child.prototype = new Father();
//创建一个实例
let Macc = new Child();
//判断实例与原型的关系
console.log(Macc instanceof Child); //true
console.log(Macc instanceof Father); //true
console.log(Macc instanceof Object); //true
分析一下实例 Macc
的原型链:
- 首先,因为Macc是Child的实例,所以
Macc instanceof Child
返回true
; - 然后,Child的原型是Father的实例,也就是说
Child.prototype.constructor === Father
可以找到Father的构造函数,因此Macc instanceof Father
返回true
; - 最后,任何函数的默认原型都是Object的实例,所以
Macc instanceof Object
是true
;
3.2 isPrototypeOf
方法
原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,该方法就会返回 true
。
4.关于方法
子类有事需要覆盖父类的方法,或者说增添父类中没有的方法。为此,这些方法必须在原型赋值后再添加到子类原型上。
function Father() {this.name = 'Macc';
}
Father.prototype.getName = function() {return this.name;
}function Child() {this.age = 18;
}
//继承父类
Child.prototype = new Father();
//改写父类的方法
Child.prototype.getName = function() {console.log('我没有名字');
};
//添加父类没有的新方法
Child.prototype.getAge = function() {console.log(this.age);
};
//创建实例
let Macc = new Child();
Macc.getName(); //我没有名字
Macc.getAge(); //18
上面代码的重点在于:上述的两个方法都是在Child的原型赋值为Father的实例之后才定义修改的。
若是以对象字面量的方式创建原型方法,会破坏之前的原型链,因为这相当于重写了原型链。
(二)盗用构造函数
盗用构造函数也称为对象伪装、经典继承。
作用:为解决原型包含引用值导致的继承问题。
基本思路:在子类的构造函数中调用父类的构造函数。
function Father() {this.colors = ['red', 'blue', 'green'];
}function Child() {Father.call(this); //盗用构造函数
}let Macc = new Child();
let Deing = new Child();console.log(Macc.colors === Deing.colors); //false
因为毕竟函数就是在特定上下文中执行代码的简单对象, 所以可以使用 apply或call
方法以新创建的对象(new的执行过程) 为上下文执行构造函数。
上面的代码中,通过使用 call
方法,Father构造函数在为Child的实例创建的新对象的上下文中执行了,这就相当于新的Child对象上运行了Father函数中的所有初始化代码。
结果就是:每个实例都会有自己的属性副本(可以看到Macc和Deing的colors属性已经不相等了)。
1.传递参数
盗用构造函数的一个优点就是:可以在子类的构造函数中向父类构造函数传递参数。
这里只要了解过abc函数 apply\bind\call\
的使用方法就明白了。
Father.call(this,'Macc'); //传递参数
为了确保父类构造函数不会覆盖子类定义的属性,可以在调用父类构造函数后再给实例添加额外的属性。
2.盗用构造函数的问题
盗用构造函数的问题和构造函数模式的问题一样:必须在构造函数中定义方法,因此函数不能被重用。
此外,还有就是子类不能访问父类原型上定义的方法。
3.复习一下
文章有点长,到这里复习一下前面的知识再继续看下面的组合继承效果会更好。
首先,第一节中我们学习了什么是对象,也就是一些与对象有关的概念;
然后,第二节中我们学习了如何创建对象,这里讲了3种模式,分别是
- 工厂模式
- 构造函数模式
- 原型模式
工厂模式我们没有详细讲,略过了,而在构造函数模式中,我们把属性和方法都赋值给 this
,然后通过 new
操作符创建实例,但是这种模式的弊端就是:其中定义的方法会在每个实例中都创建一遍,导致不同实例上的函数虽然同名但是不相等。
为了解决这个问题,我们引入了原型模式,我们将一些需要在实例之间共享的属性或方法定义在构造函数的原型上,这样解决了函数重用的问题,但是同时又带来了新的问题,假如某个定义在原型上的属性是引用类型的,那么就会在A实例修改该属性的时候在B实例上也反映出来,可是正常来说,不同的实例应该有属于自己属性副本。
为了让不同实例拥有属于自己的属性副本,又引入了盗用构造函数技术,但是盗用构造函数技术又带来了和构造函数模式一样的问题,函数不能被重用,因此,我们后面会将前面学到的原型模式和盗用构造函数技术的结合起来,也就是下面我们要学习的组合继承。
(三)组合继承
也叫伪经典继承,综合了原型链和盗用构造函数两者的优点。
基本思想:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例的属性。
function SuperType(name) {this.name = name;this.colors = ['red', 'blue', 'yellow'];
}SuperType.prototype.sayName = function() {console.log(this.name);
};function SubType(name, age) {//继承属性SuperType.call(this, name); //第二次调用父类的构造函数this.age = age;
}//继承方法
SubType.prototype = new SuperType(); //第一次调用父类的构造函数(寄生式组合继承要用到这里)
SubType.prototype.sayAge = function() {console.log(this.age);
}//创建实例
let instance1 = new SubType('Macc', 18);
let instance2 = new SubType('Deing', 16);console.log(instance1.colors === instance2.colors); //false
console.log(instance1.sayName === instance2.sayName); //true
console.log(instance1.sayAge === instance2.sayAge); //true
instance1.sayAge(); //18
instance2.sayAge(); //16
instance1.sayName(); //Macc
instance2.sayName(); //Deing
这样 既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
组合继承弥补了原型链和盗用构造函数的不足,是JS中使用最多的继承模式,而且组合继承保留了 instanceof
操作符和 isPrototypeOf
方法识别合成对象的能力。
(四)原型式继承
原型式继承的出发点是:即使不自定义类型,也可以通过原型实现对象之间的信息共享。
前面的学习可以知道,如果我们想在不同对象之间进行信息共享(属性、方法)的话,要先创建一个类型的构造函数,然后把想要共享的信息放在这个构造函数的原型上,再通过new操作符调用这个构造函数,创建实例,这样这些实例就可以共享这些属性和方法了。
但是,现在我们不想再额外的创建一个新的构造函数了(类型),却又想在对象之间共享信息,那么这种情况就要用原型式继承了。
下面这个函数就是实现原型式继承的基本思路
function object(o) {//临时构造函数function F() {}F.prototype = o;return new F();
}
上面的object函数会创建一个临时构造函数,将传入的对象赋值给这个临时构造函数的原型,然后返回这个临时类型的一个实例。
本质上:object函数是对传入的对象执行了一次浅复制。
let person = {name: 'Macc',friendsList: ['Deing', 'GTR', 'GaGa'],
};let anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friendsList.push('Rob');let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friendsList.push('Barbie');console.log(person.friendsList); // ['Deing', 'GTR', 'GaGa', 'Rob', 'Barbie']
上面的代码,person对象定义了另一个对象也应该共享的信息。把它传递给object函数之后会返回一个新对象,这个新对象的原型是person,意味着它的原型上既有原始值属性也有引用值属性,也就是说person的friendsList属性不仅仅是它自己的属性,也会跟
anotherPerson和yetAnotherPerson共享。
原型式继承适用的情况:你有一个对象,想在它的基础上再创建一个新对象。 你需要先把这个对象传给object函数,然后再对返回的对象进行适当的修改。
ES5通过增加 Object.create()
方法将原型式继承的概念规范化。
1.Object.create
该方法接收 2 个参数。
- 第一个参数:作为新对象原型的对象;
- 第二个参数:给新对象定义额外属性的对象(可选)
在只有一个参数的时候,该方法与上面的object函数效果相同。
let person = {name: 'Macc',friendsList: ['Deing', 'GTR', 'GaGa'],
};let anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friendsList.push('Rob');let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friendsList.push('Barbie');console.log(person.friendsList); // ['Deing', 'GTR', 'GaGa', 'Rob', 'Barbie']
Object.create的第二个参数和Object.defineProperties的第二个参数一样:每个新增属性都通过各自的描述符来描述。
let person = {name: 'Macc',friendsList: ['Deing', 'GTR', 'GaGa'],
};let anotherPerson = Object.create(person, {name: {value: 'Deing'}
});console.log(anotherPerson.name); //Deing
原型式继承非常适合不需要单独创建构造函数,但是仍需要在对象间共享信息的场合。
(五)寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
基本的寄生继承模式如下
//原型式继承
function object(o) {function F() {}F.prototype = o;return new F();
}function createAnother(original) {let clone = object(original); //通过调用函数创建一个新对象clone.sayHi = function() { //以某种方式增强这个对象:比如添加方法或属性console.log('hi');};return clone; //返回这个对象
}
上面这段代码中,createAnother函数(创建一个实现继承的函数)接收一个参数,就是新对象的基准对象。这个对象original会被传给object函数,然后将object函数返回的新对象赋值给clone。接着给clone对象添加一个新方法saiHi(以某种方式增强对象),最后返回这个对象(然后返回这个对象)。
object函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。
下面就是寄生式继承的使用场景
let person = {name: 'Macc',friends: ['Shelly', 'Court', 'Deing'],
};let anotherPerson = createAnother(person);
anotherPerson.sayHi(); //hi
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
let p1 = createAnother(person);
let p2 = createAnother(person);console.log(p1.sayHi === p2.sayHi); //false
(六)寄生组合式继承
组合继承存在效率问题,最主要的效率问题就是父类构造函数始终会被调用两次。
- 一次在创建子类原型时调用;
- 一次在子类构造函数中调用;
本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行的时候重写自己的原型就可以了。
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。
组合继承:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例的属性。
基本思路就是:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
其实就是使用寄生式继承来继承父类的原型,然后将返回的新对象赋值给子类原型。
下面的代码就是寄生式组合继承的核心逻辑
function inheritPrototype(subType, superType) {let prototype = object(superType.prototype); //创建对象prototype.constructor = subType; //增强对象,解决由于重写原型导致constructor属性丢失的问题。subType.prototype = prototype; //赋值对象
}
下面是寄生式组合继承的使用
//原型式继承
function object(o) {function F() {}F.prototype = o;return new F();
}
//寄生组合式继承
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.sayAge = function() {console.log(this.age);
}
上面的例子中,只调用了一次SuperType的构造函数,避免了子类原型上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof
和 isPrototypeOf
依然正常有效。
寄生式组合继承可以算是引用类型继承的最佳模式。
四、类
ES6新引入的 class
关键字具有正式定义类的能力。类是ES6中新的基础性语法糖结构。
它表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
(一)类的定义
定义类的方式有2种:类声明和类表达式。
两种方式都是使用 class
关键字加大括号。
//类声明
class Person {}
//类表达式
let Animal = class {}
与函数表达式类似,类表达式在它们被求值前也不能被引用。 但是与函数定义不同的是,函数声明可以提升,类定义不能提升。
还有一点与函数不同的是:函数受函数作用域限制,类则受块作用域限制。
1.类的构成
类可以包含 构造函数方法、实例方法、获取函数、设置函数、静态类方法。但是这些都不是必需的, 空的类定义照样有效。
默认情况下,类定义中的代码都在严格模式下执行。
类表达式的名称是可选的。 在把类表达式赋值给变量后,可以通过 name
属性取得类表达式的名称字符串。 但是不能在类表达式作用域外部访问这个标识符。
let Animal = class {};//这里类表达式的名称为空,所以说是可选的。
let Person = class PersonName { //PersonName就是类表达式的名称identify() {console.log(Person.name, PersonName.name);}
}let p = new Person();p.identify(); //PersonName PersonNameconsole.log(Person.name); //PersonName PersonName
console.log(PersonName); //报错,undefined,类表达式作用域外不能访问PersonName这个标识。
(二)类构造函数
constructor
关键字用于在类定义块内部创建类的构造函数。
方法名 constructor
会告诉解释器在使用 new
操作符创建类的实例的时候,应该调用这个函数。
类的构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。
1.实例化
使用 new
调用类的构造函数会执行如下操作:
- (1)在内存中创建一个新对象;
- (2)这个新对象内部的
[[Prototype]]
指针被赋值为构造函数的prototype
属性; - (3)构造函数内部的
this
被赋值为这个新对象(this
指向新对象); - (4)执行构造函数内部的代码(给新对象添加属性);
- (5)如果构造函数返回非空对象,则返回该非空对象,否则返回刚创建的新对象。
class Person {constructor(name) {console.log(arguments.length);this.name = name || null;}
}let p1 = new Person; //0
let p2 = new Person('Macc'); //1console.log(p1.name); //null
console.log(p2.name); //Macc
类实例化时传入的参数会用作构造函数的参数。 如果不需要参数,则类名后面的括号也是可选的。
默认情况下,类构造函数在执行之后返回 this
对象。 构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的 this
对象,那么这个对象就会销毁。
不过,如果返回的不是 this
对象,而是其他对象,那么这个被返回的对象不会通过 instanceof
操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。
class Person {constructor(name) {console.log(arguments.length);this.name = name || null;return {name: 'GaGa',age: 36};}
}let p1 = new Person('Macc');console.log(p1 instanceof Person); //false
类构造函数与构造函数的区别:调用类构造函数必须使用 new
操作符。而普通构造函数如果不使用 new
操作符调用,则以全局的 this
(通常是 window
) 作为内部对象。
调用类构造函数时如果忘记使用 new
操作符则会抛错。
类的构造函数没有什么特殊之处,实例化后,它会成为普通的实例方法。(但是作为类的构造函数,哪怕作为实例方法调用时仍然要使用 new
操作符)。
实例化后可以在实例上引用它(构造函数)
class Person {constructor() {console.log('调用构造函数');}
}
//使用类创建一个实例
let p1 = new Person(); //输出:调用构造函数
//使用对类的构造函数的引用创建一个新的实例
let p2 = new p1.constructor(); //输出:调用构造函数
//未使用new操作符,报错了
let p3 = Person.constructor(); //报错
2.把类当成特殊函数
从各方面看,ECMAScript中的类就是一种特殊的函数。
声明一个类之后,通过 typeof
操作符检测类标识符,表明它是一个函数。
class Person {constructor() {console.log('调用构造函数');}
}
console.log(typeof Person); //function
类标签符也有 prototype
属性,原型上也有一个 constructor
属性指向类自身;
console.log(Person.prototype); //{constructor: ƒ}
console.log(Person.prototype.constructor === Person); //true
与普通函数一样,可以使用 instanceof
操作符检查构造函数原型是否存在于实例的原型链中。
如前所述,类本身具有与普通构造函数一样的行为。 在类的上下文中,类本身在使用 new
调用时就会被当成构造函数。重点在于,类中定义的 constructor
方法不会被当成构造函数,在对它使用 instanceof
操作符时会返回 false
。
但是,如果在创建实例时直接将类的构造函数当成普通构造函数来使用,那么 instanceof
操作符的结果会反转。
class Person {}let p1 = new Person();
console.log(p1.constructor === Person); //true
console.log(p1 instanceof Person); //true
console.log(p1 instanceof Person.constructor); //falselet p2 = new Person.constructor(); //直接将类构造函数当普通构造函数用
console.log(p2.constructor === Person); //false
console.log(p2 instanceof Person); //fasle
console.log(p2 instanceof Person.constructor); //true
上面的例子其实对应的是再上面一点说到的一句话:类本身在使用 new
调用时会被当成构造函数。类中定义的 constructor
方法不会被当成构造函数。 这里要注意的是,在使用 new
的时候,被当作构造函数的是类本身,而不是类中定义的 constructor
方法。
类是JS的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递。
类可以像函数一样,在任何地方定义,比如说在数组中。
let classList = [class {constructor(id) {this.id_ = id;console.log(`instance ${this.id_}`);}}
];function createInstance(classDefinition, id) {return new classDefinition(id);
}let foo = createInstance(classList[0], 1433223);
与立即调用函数表达式相似,类也可以立即实例化。
let p = new class Foo {constructor(x) {console.log(x);}
}('bar'); //立即实例化console.log(p); //Foo ()
(三)实例、原型、类成员
类的语法可以非常方便的定义应该存在于实例上的成员、应该存在于原型上的成员以及应该存在于类本身的成员。
1.实例成员
在类构造函数内部,可以为新创建的实例(this
)添加自有属性。在构造函数执行完之后,仍然可以给实例继续添加新成员。
每个实例都对应唯一的成员对象,这一味着所有成员都不会在原型上共享。
class Person {constructor() {this.name = new String('Macc');this.sayName = () => console.log(this.name);this.nickname = ['mac', 'MC'];}
}let p1 = new Person(),p2 = new Person();console.log(p1.name === p2.name); //false
console.log(p1.sayName === p2.sayName); //false
console.log(p1.nickname === p2.nickname); //false
2.原型方法与访问器
为了在实例间共享方法,类定义语法把在类定义块中定义的方法作为原型方法。
class Person {constructor() {//添加到this的所有内容会存在于不同的实例上this.locate = () => console.log('instance');;}//在类块中定义的所有内容都会定义到类的原型上locate() {console.log('prototype');}
}let p1 = new Person();p1.locate(); //instance 实例上的属性会遮蔽原型上的同名属性(方法)
Person.prototype.locate(); //prototype
可以把方法定义在类构造函数中或类块中,但是不能在类块中给原型添加原始值或对象作为成员数据。
class Animal {name: 'GTR'//报错
}
像上面的这种写法就会报错。
类方法等同于对象属性,因此也可以使用字符串、符号、计算的值(可计算属性)作为键。
类定义也支持获取和设置访问器,语法行为与普通对象一样。
class Person {set name(newName) {this.name_ = newName;}get name() {return this.name_;}
}let p1 = new Person();
p1.name = 'Macc';
3.静态类方法
可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。
每个类上只能有一个静态成员。
静态类成员在类定义中使用 static
关键字作为前缀。在静态成员中,this
引用类自身。 其他所有约定跟原型成员一样。
class Person {constructor() {this.locate = () => console.log('instance');}//定义在类的原型对象上locate() {console.log('prototype', this);}//定义在类本身上static locate() {console.log('class', this);}
}let p = new Person();
p.locate();
Person.prototype.locate();
Person.locate(); //class, Person {}
静态方法非常适合作为实例工厂。
class Person {constructor(age) {this.age = age;}static create() {//使用随机年龄创建并返回一个Person实例return new Person(Math.floor(Math.random() * 100));}
}console.log(Person.create()); //Person {age: 65}
4.非函数原型和类成员
类定义并不显式支持在原型或类上添加成员数据,但是在类定义外部,可以手动添加。
class Person {sayName() {console.log(`${Person.greeting} ${this.name}`);}// name:'Macc', 上面也说过这样定义属性是错误的
}
//在类定义外部手动添加
Person.greeting = 'My name is'; //在类上定义数据成员
Person.prototype.name = 'Macc'; //在类原型上定义数据成员let p = new Person();
p.sayName(); //My name is Macc
为什么类定义中没有显式支持添加数据成员呢?
因为在共享目标(原型和类)上添加可变(可改)的数据成员是一种反模式。
一般来说,对象实例应该独自拥有通过
this
引用的数据。
5.迭代器与生成器方法
类定义语法支持在原型和类本身上定义生成器方法。
class Person {//在原型上定义生成器方法*createNickIterator() {yield 'Jack';yield 'Jake';yield 'J-Dog';}//在类上定义生成器方法static *createJobIterator() {yield 'Butcher';yield 'Baker';yield 'Canlestic maker';}
}let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); //Butcher
console.log(jobIter.next().value); //Baker
console.log(jobIter.next().value); //Canlestic makerlet p = new Person();
let nicknameIter = p.createNickIterator();
console.log(nicknameIter.next().value);
console.log(nicknameIter.next().value);
console.log(nicknameIter.next().value);
因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象。
class Person {constructor() {this.nickname = ['Jack', 'Jake', 'J-Dog'];}*[Symbol.iterator]() {yield *this.nickname.entries();}
}let p = new Person();
//把类的实例p变成了可迭代对象
for (let [idx, nickname] of p) {console.log(nickname);
}
也可以只返回迭代器实例
class Person {constructor() {this.nickname = ['Jack', 'Jake', 'J-Dog'];}[Symbol.iterator]() {//只返回迭代器实例return this.nickname.entries();}
}let p = new Person();
for (let [idx, nickname] of p) {console.log(nickname);
}
(四)继承
1.继承基础
ES6支持单继承。 使用 extends
关键字,就可以继承任何拥有 [[Constructor]]
和原型的对象。这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
//类
class Vehicle {}
//继承类
class Bus extends Vehicle {}let bus = new Bus();
console.log(bus instanceof Bus); //true
console.log(bus instanceof Vehicle); //true//构造函数
function Person() {}
//继承普通构造函数
class Chinese extends Person {}let Macc = new Chinese();
console.log(Macc instanceof Person); //true
console.log(Macc instanceof Chinese); //true
类和原型上定义的方法都会带到派生类。 this
的值会反映调用相应方法的实例或者类。
class Vehicle {identifyPrototype(id) {console.log(id, this);}static identifyClass(id) {console.log(id, this);}
}class Bus extends Vehicle {}let v = new Vehicle();
let b = new Bus();b.identifyPrototype('bus');
v.identifyPrototype('vehicle');Bus.identifyClass('Bus');
Vehicle.identifyClass('Vehicle');
extends
关键字也可以再类表达式中使用,如 let Bar = class extends Foo{}
2.构造函数、HomeObject
、Super()
派生类的方法可以通过 super
关键字引用它们的原型。
这个关键字只在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
在类构造函数中使用 super
可以调用父类构造函数。
class Vehicle {constructor() {this.hasEngine = true;}
}class Bus extends Vehicle {constructor() {//不要再调用super前引用this,否则会报错!!!super(); //相当于 super.constructor()console.log(this instanceof Vehicle); //trueconsole.log(this); //Bus {hasEngine : true}}
}new Bus();
在静态方法中可以通过 super
调用继承的类上定义的静态方法。
class Vehicle {static identify() {console.log('vehicle');}
}class Bus extends Vehicle {static identify() {super.identify();}
}Bus.identify(); //vehicle
ES6给类构造函数和静态方法添加了内部特性 [[HomeObject]]
,这个特性是一个指针,指向定义该方法的对象。 这个指针是自动赋值的,而且只能在JS引擎内部访问。super始终会定义为 [[HomeObject]]
的原型。
2.1 super
的使用注意事项
(1) super
只能在派生类的构造函数和静态方法中使用。
(2)不能单独引用 super
关键字,要么用它调用构造函数,要么用它调用静态方法。
(3)调用 super()
会调用父类构造函数,并将返回的实例赋值给 this
。
(4)super()
的行为如同调用构造函数,如果需要给父类的构造函数传递参数,需要手动传入。
class Vehicle {constructor(color) {this.color = color;}
}class Bus extends Vehicle {constructor(color) {super(color); //向父类构造函数传递参数}
}console.log(new Bus('black')); //Bus {color: 'black'}
(5)如果没有定义(子)类构造函数,在实例化派生类时会调用 super()
,而且会传入所有传给派生类的参数。
class Vehicle {constructor(color) {this.color = color;}
}//子类没有定义构造函数
class Bus extends Vehicle {}console.log(new Bus('black')); //Bus {color: 'black'}
(6)在类构造函数中,不能在调用 super()
前调用 this
。
(7)如果在派生类中显式定义了构造函数,则要么必须在其中调用 super()
,要么必须在其中返回一个对象。
class Vehicle {}
//car没有定义构造函数
class Car extends Vehicle {}class Bus extends Vehicle {constructor() {super(); //要么调用super}
}class Van extends Vehicle {constructor() {return {}; //要么返回一个对象}
}console.log(new Car()); //Car {}
console.log(new Bus()); //Bus {}
console.log(new Van()); //{}
3.抽象基类
有时候可能需要定义一个这样的类:它可供其他类继承,但本身不会被实例化。
在ECMAScript中可以通过 new.target
实现,new.target
保存通过new关键字调用的类或函数。 通过在实例化时检查 new.target
是不是抽象基类,可以阻止对抽象基类的实例化。
//抽象基类
class Vehicle {constructor() {console.log(new.target);if (new.target === Vehicle) {throw new Error('抽象基类不可以实例化');}}
}//派生类
class Bus extends Vehicle {}//实例化派生类
new Bus();
//实例化抽象基类
new Vehicle();
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。
因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this
关键字来检查相应方法。
//抽象基类
class Vehicle {constructor() {if (new.target === Vehicle) {throw new Error('抽象基类不可以实例化');}if (!this.foo) {throw new Error('派生类必须定义foo方法');}console.log('success');}
}//定义了foo方法的正常
class Van extends Vehicle {foo() {}
}
//派生类:未定义foo方法,会报错
class Bus extends Vehicle {}new Van();
new Bus(); //报错
4.继承内置类型
开发者可以很方便地扩展内置类型。
class SuperArray extends Array {shuffle() {//洗牌算法for (let i = this.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));[this[i], this[j]] = [this[j], this[i]];}}
}let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); //true
console.log(a instanceof SuperArray); //true
console.log(a); //SuperArray(5) [1,2,3,4,5]
a.shuffle();
console.log(a); //SuperArray(5) [5, 1, 2, 3, 4]
有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例类型是一致的。
class SuperArray extends Array {}let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x % 2));console.log(a1);
console.log(a2);
console.log(a1 instanceof SuperArray); //true
console.log(a2 instanceof SuperArray); //true
如果想要覆盖这个默认行为, 则可以覆盖 Symbol.species
访问器, 这个访问器决定在创建返回的实例时使用的类。
class SuperArray extends Array {static get[Symbol.species]() {return Array;}
}let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x % 2));console.log(a1);
console.log(a2);
console.log(a1 instanceof SuperArray); //true
console.log(a2 instanceof SuperArray); //false 注意这里的结果与上面的结果不同
5.类混入
把不同类的行为集中到一个类时一种常见的JS模式。
Object.assign()
方法就是为了混入对象行为而设计的。如果只是需要混入多个对象的属性,那么使用这个方法就可以了。
在下面的代码中,extends
关键字后面是一个JS表达式。 任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时被求值。
class Vehicle {}function getParentClass() {console.log('求值表达式');return Vehicle; //返回一个类
}class Bus extends getParentClass() {}
混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会被解析为一个可以被继承的类。
如果Person类需要组合A、B、C,则需要某种机制来实现B继承A,C继承B,而Person再继承C,从而把ABC组合到这个超类。
一种策略是:定义一组“可嵌套”函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。 这些组合函数可以连缀调用,最后组合成超类表达式。
class Vehicle {}
//一组可嵌套函数
let FooMixin = (SuperClass) => class extends SuperClass {foo() {console.log('foo');}
};let BarMixin = (SuperClass) => class extends SuperClass {bar() {console.log('bar');}
};let BazMixin = (SuperClass) => class extends SuperClass {baz() {console.log('baz');}
};class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}let bus = new Bus();
bus.foo();
bus.bar();
bus.baz();
通过写一个辅助函数,可以把嵌套调用展开。
//辅助函数
function mix(BaseClass, ...Mixins) {return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
很多JS框架已经抛弃混入模式, 转向了复合模式。 复合模式就是把方法提取到独立的类和辅助对象中,然后把它们组合起来,但是不使用继承。 这反映了软件设计原则:复合胜过继承。
希望看到这里的你,能有所收获。
完结,撒花…
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- 实现一个可迭代的栈
文章目录范围for循环Iterable可迭代栈实现测试运行范围for循环 集合类数据类型的基本操作之一就是迭代遍历并处理集合中的每个元素。 C提供了范围for循环语句实现这个操作(从头到尾依次遍历元素)。 一个可用于范围for循环(可迭代࿰…...
2024/5/3 3:02:14 - 【HTML】html基本操作
1.标题 eg:<h1>一号标题</h1> 加tab变标签 从h1-h6依次增大字号。 自己造标题不好用的啦,无法运行的。 2.空格和回车不能直接用键盘的,要用代码 一个空格 一个换行,一个回车 <br /> 3.测试细节以浏览器最终效果为主 4.…...
2024/4/19 16:55:30 - 时区时间配置
查看当前的时间和时区 查看所有的时区:timedatectl list-timezones 修改时区为东八区: timedatectl list-timezones | grep Shanghai设置时区 timedatectl set-timezone Asia/Shanghai同步网络时间 修改关闭ntp自动同步timedatectl set-ntp false 手动设置时间 …...
2024/5/3 6:35:07 - linux下使用nohup挂后台启动django项目
django在linux上运行,一般在xshell远程连接后,是通过如下命令启动服务: python manage.py runserver 0.0.0.0:8000 但是这样有个弊端,窗口关闭服务就停止了,如何做到窗口关闭服务不停止呢?那就是后台启动…...
2024/4/13 6:18:24 - HE4484E芯片资料
HE4484E是一款5VUSB适配器输入,高精度双节锂离子电池充电管理芯片。具有0V充电功能,涓流充电、恒流充电、恒压充电和自动截止、自动再充等一套完整充电循环的充电管理芯片。芯片内部特设9V抗浪涌,芯片应用更安全可靠。HE4484E标准浮充电压为8…...
2024/4/23 0:23:44 - git 远程仓库时报错SSL certificate problem: unable to get local issuer certificate(git版本与Let‘s Encrypt的证书导致)
起因: 之前自己的远程gitea服务一直用的IP端口号进行使用,基本操作都没有问题。在加上SSL改为(https:\\域名)访问后浏览器页面访问正常,但进行git命令操作时报错“SSL certificate problem: unable to get local issue…...
2024/4/13 6:18:39 - 112 Stucked Keyboard (20 分)
题目大意:如果一个键盘卡住,就会连续输出k次,判断哪些键盘卡住了,并且输出原序列 注意点:假如一个字符出现过连续k次和不是连续k次的,那么这个键就是好的。(k的倍数) #include <iostream> #include …...
2024/4/13 6:18:39 - 发什么视频可以吸引粉丝
抖音快手粉丝运营的好的大v都知道,现在运营短视频的话,使用智能剪辑软件比如王者剪辑,自动化剪辑视频,可以只要一个人也能够完成同时运营多个抖音快手号,是最有效的涨粉秘诀,海量粉丝博主就是用这种技巧。今…...
2024/4/19 21:39:56 - centos7-mysql5.7安装
1. mysql下载 wget https://mirrors.tuna.tsinghua.edu.cn/mysql/downloads/MySQL-5.7/mysql-5.7.35-1.el6.x86_64.rpm-bundle.tar 或者本地下载后再上传linux服务器 2. 检测是否安装过mysql rpm -qa | grep mysql 如果存在就卸载 rpm -e --nodeps mysql57-community-rel…...
2024/4/25 13:02:47 - 子网划分.
一、前言: 1.IP地址: (1).都是32位二进制(四段8位二进制组成)(4个字节<一个字节:8个零或一>),十进制表示:四个中间用点分隔的十进制数表示(点分十进制记法<dotted decimal notation>)。 (2).首先我们知…...
2024/4/16 11:51:53 - ffmpeg学习 源代码编译、英伟达硬件加速
使用cpu进行软编解码时,cpu效率低并且占用高。使用硬件加速,能够明显降低CPU的占用,参看博客 ffmpeg学习(16)AVDevice使用。 这里以使用英伟达gpu进行h264编解码加速为例说明,其他平台类似。 1、winodws硬…...
2024/4/17 14:40:26 - 微服务中网关(API Gateway)的技术选型
用 Spring Cloud 微服务实战中,大家都知道用 Zuul 作为智能网关。API 网关(API Gateway)主要负责服务请求路由、组合及协议转换。下面是大家的总结:一、最佳回答网关的技术选型SpringCloud-Zuul :社区活跃,基于 SrpingCloud 完整生态, 是构建微服务体系前置网关服务的最佳选…...
2024/4/13 7:23:20 - Spring事件监听机制的使用和源码解读
目录 前言 正文 Spring事件监听机制的使用 理论 实操之同步调用 实操之异步调用 总结 Spring事件监听机制源码解读 总结 前言 在很多公司可能在事件回调机制上,并发不高的情况下会使用到Spring的事件监听机制来回调,那么本帖来介绍和使用Sprin…...
2024/4/8 18:59:08 - C Primer Plus 第13章(文件输入输出)
目录1. 与文件进行通信1.1 文件是什么1.2 文本模式和二进制模式1.3 I/O 的级别1.4 标准文件2. 标准 I/O2.1 检查命令行参数2.2 fopen() 函数2.3 getc() 和 putc() 函数2.4 文件结尾2.5 fclose() 函数2.6 指向标注文件的指针3. 文件拷贝4. 文件 I/O:fprintf() 、fsca…...
2024/4/14 23:13:48 - 靶场练习第十三天~vulnhub靶场之dc-5
一、准备工作 1.靶机环境搭建 下载链接: https://pan.baidu.com/s/1csvuJ_NVCBvVr75KhxyM3Q?pwdxie7 提取码: xie7 2.kali的ip 命令:ifconfig 3.kali和靶机的都设置为NAT模式 二、信息收集 1.nmap的信息收集 (1)寻找靶机的ip 命令&…...
2024/4/5 2:08:09 - Runtime模块管理运行环境,可用于获取当前运行环境信息、与其它程序进行通讯等。通过plus.runtime可获取运行环境管理对象
属性: appid: 当前应用的APPIDarguments: 第三方程序调用时传递给程序的参数channel: 应用的渠道标识launcher: 应用启动来源origin: 应用安装来源version: 客户端的版本名称versionCode: 客户端的版本号innerVersion: 客户端5运行环境的内部版本号uniVersion: 客户…...
2024/4/20 8:00:32 - OWASP juice shop靶场闯关题解
1、找到隐藏的记分板 /#/score-board 会直接跳转到计分板 2、登陆管理员账户 这里可以尝试用sql注入的方式进入管理员界面 1、无账号,无密码进入 ’ or 00 – 2、有账号,无密码进入**adminjuice-sh.op--**3、xss攻击 在搜索框输入<iframe src“javascript:aler…...
2024/4/23 18:36:41 - scapy双线程,阻塞监听发包,握手挥手,发送http请求
涉及知识 1.握手和挥手的本质 前言: 其实本人觉得挥手握手啥的完全是混淆概念,序列号和验证号的变化完全取决于你是数据发送方还是接收方。 这里的数据仅仅指的是应用层http里携带的的数据,不包括TCP和IP包头。 知识点1 假设你在发送数据&…...
2024/4/5 2:08:06 - 算法训练 审美课 java 题解 194 (哈希)
问题描述 《审美的历程》课上有n位学生,帅老师展示了m幅画,其中有些是梵高的作品,另外的都出自五岁小朋友之手。老师请同学们分辨哪些画的作者是梵高,但是老师自己并没有答案,因为这些画看上去都像是小朋友画的……老师…...
2024/4/13 6:19:14 - Android通过adb命令传参给APP的方法
老套路先看图 说下原理: 使用adb命令启动server然后传递参数,service拿到参数后可以根据需求实现模拟控制APP 看代码 package cn.yhsh.adbinputserver.service;import android.app.NotificationChannel; import android.app.NotificationManager; imp…...
2024/4/28 19:09:43
最新文章
- SpringBoot的ProblemDetails
1.RFC 7807 之前的项目如果出现异常,默认跳转到error页面。或者是抛出500 异常。 但是对于前后端分离的项目,Java程序员不负责页面跳转,只需要 把错误信息交给前端程序员处理即可。而RFC 7807规范就是将异常 信息转为JSON格式的数据。这个…...
2024/5/3 8:37:06 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - PCF8591(ADDA转换芯片)
工具 1.Proteus 8 仿真器 2.keil 5 编辑器 原理图 讲解 PCF8591是一个单片集成、单独供电、低功耗、8-bit CMOS数据获取器件。PCF8591具有4个模拟输入、1个模拟输出和1个串行IC总线接口。PCF8591的3个地址引脚A0, A1和A2可用于硬件地址编程,允许在同个I2C总线上接…...
2024/4/30 17:12:59 - 蓝桥杯加训
1.两只塔姆沃斯牛(模拟) 思路:人和牛都记录三个数据,当前坐标和走的方向,如果人和牛的坐标和方向走重复了,那就说明一直在绕圈圈,无解 #include<iostream> using namespace std; const i…...
2024/5/1 13:10:13 - Jenkins 使用 Description Setter
想要的效果如图: 在打包完成之后直接在构件历史的部分展示出来构建的docker镜像名,这样就不需要去找日志了。 首先安装插件 Description Setter, 如何安装就不在此赘述了。 安装完成之后,在构件后操作选项添加一个流程, 有两个字段: regular expressi…...
2024/5/1 4:32:53 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/5/1 17:30:59 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/5/2 16:16:39 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/4/29 2:29:43 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/5/2 9:28:15 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/4/27 17:58:04 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/4/27 14:22:49 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/4/28 1:28:33 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/4/30 9:43:09 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/4/27 17:59:30 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/5/2 15:04:34 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/4/28 1:34:08 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/4/26 19:03:37 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/4/29 20:46:55 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/4/30 22:21:04 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/5/1 4:32:01 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/4/27 23:24:42 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/4/28 5:48:52 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/4/30 9:42:22 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/5/2 9:07:46 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/4/30 9:42:49 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57