数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4个特性描述它们的行为。
Configurable
用来表示属性是否可以通过delect 删除 并重新定义,是否可以修改它的特性,以及是否可以给他修改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
Enumerable
用来表示属性是否可以通过for-in循环,默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面例子所示。
value
包含属性实际的值,这就是前面提到的那个读取和写入属性值的位置。这个特性默认为undefined
let person = {};
Object.defineProperty(person,"name",{
writable : false,
configurable: false,
value:"Nicholas"
});
console.log(person.name) // "Nicholas"
person.name = "lilei";
console.log(person.name) // "Nicholas"
//writable : false, 只读,这个例子创建了一个名为 name 的属性并给它赋予了一个只读的值"Nicholas"。这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误
delete person.name;
console.log(person.name) // "Nicholas"
// configurable:false 代表不能删除
。
访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。
在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效
的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。
Configurable
表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。
Enumerable
表示属性是否可以通过 for-in 循环返回。
Get
获取属性,在读取属性时调用。默认值undefined。
Set
设置函数,在写入属性时调用,默认值为undefined。
访问器属性是不能直接定义的,必须使用 Object.defineProperty()。
let lilei = {
name: "lilei",
age_: 18
};
Object.defineProperty(lilei, "age", {
get() {
return this.age_;
},
set(newValue) {
if (newValue > 22) {
this.age_ = newValue; // 符合规则修改变量
} else {
//直接 报错
}
}
})
lilei.age = 23;
console.log(lilei.age); //输出23
在这个例子中,对象 lilei 有两个默认属性:name 和 age_。 age_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。一个属性 age 被定义为一个访问器属性,其中获取函数简单地返回 age_的值,而设置函数会做一些 处理方法 当符合规则才可以修改 成功。
定义多个属性
Object.defineProperties()
let book = {};
Object.defineProperties(book, {
year_: {
value: 2022
},
edititon: {
value: 1
},
year: {
get() {
return this.year_;
},
set(){
if(newValue >2022){
this.year_ = newValue;
}
}
}
})
//这段代码在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。
读取属性的特性
Object.getOwnPropertyDescriptor()
使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。
返回值是一个对象,对于访问器属性包含configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、writable 和 value 属性。
let book = {
};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_;
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor);
console.log(descriptor.value);
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor2 = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor2);
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
// ES7中 新增了 Object.getOwnPropertyDescriptors()静态方法
console.log(Object.getOwnPropertyDescriptors(book));
// 它会将每个属性的详情输出出来。
- 对于数据属性year_ ,value 等于原来的值,configurable是false, get()是undefined
- 对于访问器属性 year,value 是undefined ,enumerable 是 false,get 是一个指向获取函数的指针。
- ES7中 新增了 Object.getOwnPropertyDescriptors()静态方法
- 这个方法实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。
合并对象
JavaScript 开发者经常觉得“合并”(merge)两个对象很有用。更具体地说,就是把源对象所有的本地属性一起复制到目标对象上。
有时候这种操作也被称为“混入”(mixin),因为目标对象通过混入源对象的属性得到了增强。
Object.assign()
- ECMAScript 6 专门为合并对象提供了 Object.assign()方法。
- 这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。
- 以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。
- Object.assign()实际上对每个源对象执行的是浅复制。
如果多个源对象都有相同的属性,则使用最后一个复制的值。- 此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目
标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
let dest, src, result;
/*
简单复制
*/
dest = {};
src = { id: "src" };
result = Object.assign(dest, src);
Obejcet.assign 修改目标对象
//也会返回 修改后的目标对象
console.log(result);
console.log(dest == result); // true
console.log(dest !== src); //true
console.log(dest);
//多个源对象
dest = {};
result = Object.assign(dest, { a: "foo" }, { b: "bar" });
console.log(result);
//获取函数与设置函数
dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`);
}
}
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
}
Object.assign(dest, src);
//可以通过目标 对象上的设置函数 观察到覆盖的过程
dest = {
set id(x) {
console.log(x);
}
}
Object.assign(dest, { id: "first" }, { id: "second" }, { id: "third" });
对象引用
如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()没有“回滚”之前
赋值的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
对象标识相等判定
在ECMAScript6 之前,有些特殊情况即使是 === 操作符也 无能为力。
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
要检查超过两个值,递归地利用相等性传递即可:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
增强对象语法
ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。
属性值的简写
在给对象添加变量的时候开发者经常会发现属性名和变量名是一样的。
let name = "Matt";
let person = {
name:name;
}
console.log(person); // { name: 'Matt' }
// 简写属性名语法出现了,简写属性名只要使用变量名(不在用变量名)就会自动被解释为同名的属性键。 如果没有就会抛出错误。
let person = {
name
}
console.log(person); // { name: 'Matt' }
//代码压缩程序会在不同作用域间保留属性名,以防止找不到引用。
function makePerson(name){
return{
name
}
}
let person = makePerson("Matt");
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
//在这里,即使参数标识符只限定于函数作用域,编译器也会保留初始的 name 标识符。如果使用
//Google Closure 编译器压缩,那么函数参数会被缩短,而属性名不变:
//在这里,即使参数标识符只限定于函数作用域,编译器也会保留初始的 name 标识符。
可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
有了可计算属性,就可以在对象字面量中完成动态属性赋值。
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
//console.log(person); //{ name: 'Matt', age: 27, job: 'Software engineer' }
因为被当作javascript表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化的时候在求值。
const nameKey = "name";
const ageKey = "age";
const jobKey = "job";
let uniqueToken = 0;
function getUniqueKey(key){
return `${key}_${uniqueToken++}`
}
let person = {
[getUniqueKey(nameKey )] : "Matt",
[getUniqueKey(ageKey )] : "27",
[getUniqueKey(jobKey)] : "software engineer"
}
console.log(person);
简写方法名
在给对象定义方法的时,通常都要写一个方法名、冒号,然后再引出一个匿名函数表达式。
let person = {
sayName : function(name){
console.log('My name is ${name}')
}
};
person.sayName('Matt');
//简写方法
let person = {
sayName(){
console.log('My name is ${name}');
}
}
//简写方法名对获取函数 和设置函数也是适用的。
let person = {
name_:"",
get name(){
return this.name_;
},
set name(name){
this.name_ = name;
},
sayName(){
console.log('My name is ${this.name_}')
}
}
person.name = 'Matt';
person.sayName() //My name is Matt
//简写方法名与可计算属性建相互兼容
const methodKey = 'sayName';
let person = {
[methodKey](name){
console.log("My name is ${name}");
}
}
person.sayName("Matt"); // My name is Matt
对象解构
ECMAScript 6 新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。
// 不适用对象解构的
let person = {
name: "Matt",
age:27
}
let personName = person.name,personAge = person.age;
console.log(personName); // Matt
console.log(personAge); // 27
// 使用对象解构的:
let person = {
name: "Matt",
age : 27
}
let {name:personName,age:personAge} = person;
console.log(personName);
console.log(personAge);
let {name,age} = person;
console.log(name); //Matt
console.log(age); //27
解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是 undefined。
let { name, job } = person;
console.log(job); // undefined
也可以在解构赋值的同时定义默认值
let { name, job='Software engineer' } = person;
console.log(job); // Software engineer
解构在内部使用函数ToObject()把元数据结构转换为对象。意味着在对象解构的上下文中,原始值会被当成对象。根据ToObject()函数的定义,null和undefined不能被解构,否则会抛出错误。
let {length} = "foobar";
console.log(length)
包装对象
上面代码中,foobar 是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。
let { constructor: c } = 4;
console.log(c === Number);
解构并不要求变量必须在解构表达式中声明。不过,如果是给事先声明的变量赋值,则赋值表达式
必须包含在一对括号中:
let personName, personAge;
let person = {
name : "Matt",
age:27
}
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27
嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制,为此,可以通过解构来复制对象属性。
let person = {
name: "Matt",
age: 27,
job: {
title: 'Software engineer'
}
}
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给 personCopy,所以修改
// person.job 对象的属性也会影响 personCopy
person.job.title = 'Hacker'
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
解构赋值可以使用嵌套结构,以匹配嵌套的属性:
let person = {
name: "Matt",
age:27,
job:{
title : "Software engineer"
}
}
在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样:
let person = {
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// foo 在源对象上是 undefined
({
foo: {
bar: personCopy.bar
}
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job 在目标对象上是 undefined
({
job: {
title: personCopy.job.title
}
} = person);
// TypeError: Cannot set property 'title' of undefined
部分解构
需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及
多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分。
let person = {
name: 'Matt',
age: 27
};
let personName,personBar,personAge;
try{
//person.foo 是undefined
({ name: personName, foo: { bar: personBar }, age: personAge } = person);
}catch (e) { }
console.log(personName, personBar, personAge);
//Matt, undefined, undefined
// 解构到 foo: { bar: personBar } 发生错误 后面的解构 就不会执行 ,可以将位置互换 age换到前面 personAge就不熟undefined;
参数上下文匹配
在函数参数列表中也可以进行解构赋值,对于参数的解构赋值不会影响到arguments对象,但可以在函数签名中声明在函数体内使用局部变量。
let person = {
name: 'Matt',
age:27
}
function printPerson(foo, { name, age }, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, { name: personName, age: personAge }, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
创建对象
虽然使用Object构造函数或字面量对象可以方便的创建对象,但是这种方式存在很明显的缺点:创建具有同样接口的多个对象需要重复编写很多代码。
工厂模式
工厂模式 广泛用于软件工程领域,用于抽象特定对象的过程。
function createPerson(name, age, job) {
let o = new Object;
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
这里,函数 createPerson()接收 3 个参数,根据这几个参数构建了一个包含 Person 信息的对象。
可以用不同的参数多次调用这个函数,每次都会返回包含 3 个属性和 1 个方法的对象。这种工厂模式虽
然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
在这个例子中,Person()构造函数代替了 createPerson()工厂函数。
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
person1 和 person2 分别保存着 Person 的不同实例。这两个对象都有一个constructor 属性指向 Person。
instanceof 操作符可以确定对象的类型
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
构造函数不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:
let Person = function(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name)
}
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以
调用相应的构造函数:
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person();
let person2 = new Person;
person1.sayName(); // Jake
person2.sayName(); // Jake
构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。
除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。
任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到 window 对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
构造函数的问题
构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。
我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对象。
遍。因此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例。
我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数实际上是这样的:
这样理解这个构造函数可以更清楚地知道,每个 Person 实例都会有自己的 Function 实例用于显
示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function
实例的机制是一样的。因此不同实例上的函数虽然同名却不相等
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}
console.log(person1.sayName == person2.sayName); // false
因为都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。
要解决这个问题,可以把函数定义转移到构造函数外部:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
在这里,sayName()被定义在了构造函数外部。在构造函数内部,sayName 属性等于全局 sayName()函数。
因为这一次 sayName 属性中包含的只是一个指向外部函数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函数。
这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。
如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。
这个新问题可以通过原型模式来解决。
原型模式
每个函数都会创建一个prototype属性,这一个属性是一个对象,包含应该由特定引用类型的实例
原形层级
- 通过对象访问属性时,会按照这个属性的名称开始搜索,搜索开始于对象实例本身,如果自身存在这个属性就会返回对应的值,
- 如果对象自身没有这个属性 就会沿着指针进入原型对象,在原型对象找到该属性后再返回对应的值。
- 这就是原型用于再多个对象实例间共享属性和方法的原理。
- 如果自身有跟原型有 同名属性,自身就会遮罩到原型属性。
function Person(){}
Person.prototype.name = "liu";
Person.prototype.age = 28;
Person.prototype.job = "Software"
Person.prototype.sayName = function(){
console.log(this.name);
}
let person1 = new Person()
let person2 = new Person()
person1.name = "ggg"; // person1 的name 属性遮罩了原型属性
console.log(person1.name); // ggg
console.log(person2.name); // liu
可以通过 delect 方法删除新增的属性,就会依然从原型对象中查找属性。
delect person1.name;
console.log(person1.name); // liu
hasOwnProperty()
hasOwnProperty 方法用于确定某个属性是在实例上还是在原型对象上。
如果hasOwnProperty 访问的 是实例的值 就会返回true 否则返回false。
function Person() { }
Person.prototype.name = "liu";
Person.prototype.age = 28;
let person1 = new Person();
person1.name = "ggg";
console.log(person1.hasOwnProperty("name")); // true name 属性属于自身的。
delete person1.name;
console.log(person1.hasOwnProperty("name")); // false name 属性为原型对象的。
原型和 in操作符
有两种方式使用in 操作符:单独使用和 在for-in 循环中用。
单独使用in 操作符
单独用in操作符 ,可以来判断 一个属性是否 是对象内的属性,无论是对象的实例属性,还是原型属性,只要存在 就会返回true 否则false;
function Person() { }
Person.prototype.name = "liu";
Person.prototype.age = 28;
let person1 = new Person();
person1.name = "ggg";
console.log("name" in person1) // true 当前实例上存在这个属性。 来自当前实例
delect person1.name;
console.log("name" in person1) // true 来自原型。
在for-in 循环中使用in操作符
在for-in 循环中使用in操作符 可以访问对象,用来返回可以被枚举的属性,包括原型属性和实例属性。
实例上 或者 原型上的属性为(enumerable = false)就不可以枚举。
function Person() { }
Person.prototype.name = "liu";
Person.prototype.age = 28;
Person.prototype.job = "Software"
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person()
let person2 = new Person()
// person1.name = 'gg'
Object.defineProperty(person1, "name", {
enumerable: false
})
for (var x in person1) {
console.log(x);
}
//age
//job
//sayName
Object.keys();
Object.keys() 接收一个对象作为参数,返回包含所该对象所有属性的字符串数组。
Object.keys(); 不能访问原型上的属性,只能访问当前实例的熟悉。
let person1 = { name: "liu", age: 28, job: "Software" }
let keys = Object.keys(person1);
console.log(keys);
//['name', 'age', 'job']
function Person() { };
Person.prototype.name = "liu";
Person.prototype.age = 28;
Person.prototype.job = "Software"
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person()
person1.name = "aa";
let keys = Object.keys(person1);
console.log(keys); // [name]
列出所有实例属性 Object.getOwnPropertyNames()
如果想要列出所有实例自身属性,无论是否可以枚举,可以使用Object.getOwnPropertyNames()方法
console.log(.getOwnPropertyNames(Person)); //['length', 'name', 'arguments', 'caller', 'prototype']
ES6 针对 Symbol 的 Object.getOwnPropertySymbols()
在Es6 中新增的 Symbol类型之后,相应的新增了Object.getOwnPropertySymbols() 对于符号的适配。
属性枚举顺序
for-in 循环、Object.keys 、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols以及Object.assgin 在属性枚举顺序上有很大的区别。
for-in 循环 和Object.keys 的枚举顺序是不确定的,根据浏览器差异不同。
、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols以及Object.assgin 枚举顺序是确定的,现已升序枚举数值键,然后以插入顺序枚举字符串和符号键。
let k1 = Symbol("k1");
let k2 = Symbol("k2");
let o = {
1: 1,
first: "first",
[k1]: "sym2",
second: "second",
0: 0
}
o[k2] = "sym2"
o[3] = 3
o.third = "third";
o[2] = 2
console.log(Object.getOwnPropertyNames(o));
// ['0', '1', '2', '3', 'first', 'second', 'third'];
console.log(Object.getOwnPropertySymbols(o)); //[Symbol(k1), Symbol(k2)]
对象迭代
Es7 新增了两个迭代方法:
Object.values()方法 Object.entries()方法
- 用于将对象内容转换为序列化的 可迭代的格式。
- 非字符串的属性会被转换为字符串输出。
- 这个方法还会执行对象的浅复制。
const o = {
foo: "bar",
baz: 1,
qux: {}
}
console.log(Object.values(o)); // ['bar', 1, {}]
console.log(Object.entries(o)); //[Array(2), Array(2), Array(2)]
//浅复制
console.log(Object.values(o)[0] === o.foo); //true
console.log(Object.entries(o)[0][1] === o.foo); //true
其他原型语法
直接通过一个包含所有属性和方法的对象字面量来重写原型。
function Person() { }
Person.prototype = {
//给原型赋值的constructor的 enumberable = true 可以被枚举出来
constructor:Person,
name: "ku",
age: 29,
job: "js",
sayName() {
console.log(this.name);
}
}
let friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Object); //false
console.log(friend.constructor == Person); //true
// 恢复 constructor
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
原型的动态性
- 因为从原型上搜索值得过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
- 在执行 firend.sayHi() 时,自身没有这个属性他就会从原型对象中搜索这个属性。
- 实例和原型的之间的链接 是一个简单的指针 ,而不是保存的副本。
- 重写原型会切断最初原型与构造函数的联系,实例就不能指向原来的对象。
- 因为最开始创建的实例 指向的是 { } 的person实例,这个原型并没有 sayHi 方法。
function Person() { }
let firend = new Person();
Person.prototype.sayHi = function () {
console.log("hi");
}
firend.sayHi(); //hi
// 重写原型会切断最初原型与构造函数的联系,实例就不能指向原来的对象。\
Person.prototype = {
sayHi(){
console.log(hi);
}
}
firend.sayHi(); //firend.sayHi is not a function
原生对象原型
原型模式之所以重要,不仅仅体现在自定义类型上,而且它也是实现所有原声引用类型的模式。
所有原声引用类型的构造函数包括(Object、Array、String等) 都是在原型定义了实例化的方法。
可以通过prototype 为原型添加新的方法。
不推荐直接修改原始值得类型。
console.log(typeof Array.prototype.sort); //function
console.log(typeof String.prototype.substring); // function
通过prototype给原始值包装类型添加一个新的属性
String.prototype.startsWith = function (text) {
return this.indexOf(text) === 0
}
let msg = "hello world";
console.log(msg.startsWith("hello")); // true
原型的问题
- 弱化了向构造函数传递初始化参数的能力,会导致所有实例的默认值都取得相同的属性
- 原型的共享特性 原型上的所有属性在实例间是共享的,实例1修改原型上的数据会影响到所有原型的所有实例。
Person.prototype = {
constructor: Person,
name: "li",
friends: ["Shelby", "Court"],
sayName(){
console.log(this.name);
}
}
let person1 = new Person();
let person2 = new Person();
person1.friends.push("van");
console.log(person1.friends); // ['Shelby', 'Court', 'van']
console.log(person2.friends); // ['Shelby', 'Court', 'van']
console.log(person1.friends === person2.friends) //true
继承
ECMAScript 是通过原型链的方式实现的。
构造函数、原型和实例的关系
- 每个构造函数都有一个原型对象,原型有一个属性指回构造函数 constructor
- 而实例内部有一个指针指向原型 prototype
- 如果原型是另一个类型的实例?那就意味着这个原型本身就有一个指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。
- 这样就在实例和原型之间够早了一条原型链。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty = false;
}
console.log(SubType.prototype); // 打印一下 继承前的原型 constructor -> SubType
// 继承SuperType
SubType.prototype = new SuperType();
//console.log("SubType",SubType.prototype); 改变了SubType的原型为 SuperType。
SubType.prototype.getSubValue = function () {
return this.subproperty;
}
let instance = new SubType();
console.log("instance",instance.getSuperValue()) // true.
通过原型链实现继承以后,搜索就可以继承向上搜索原型的原型。
默认原型
- 默认情况下,所有引用类型都继承自Object,这是通过原型链实现的。
- 这也是任何自定义类型能够继承包括toString() valueOf() 在内的方法。
原型与继承的关系
原型和实例的关系可以通过两种方式来确定
- 使用instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则instanceof 返回true
- 使用isPrototypeOf()方法。原型链中的每个原型都可以调用它。
// 来自上面的
//instanceof
let instance = new SubType();
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true
// 因为 instance 是Object SuperType 和SubType 的实例
// instance 的原型链中 包含这些原型
Object.isPrototypeOf(instance) // true
SuperType.isPrototypeOf(instance) // true
SubType.isPrototypeOf(instance) // true
关于方法
子类有时候需要覆盖父类的方法,或者增加父类没有的方法,这些方法必须在原型赋值之后再添加到原型上。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 新方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
}
// 覆盖已有的方法
SubType.prototype.getSuperValue = function () {
return false
}
let instance = new SubType();
console.log(instance.getSuperValue()); // false
// 现在访问的就是后面遮罩的方法。
以对象字面量的方法创建原型方法,会破坏之前的原型链,因为相当于重写了原型链。
// 关于方法
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SubType();
// 通过对象字面量添加新方法,这会导致上一行代码无效
SubType.prototype = {
getSubValue() {
return this.subproperty;
},
someOtherMethod() {
return false
}
}
let instance = new SubType();
console.log(instance.getSuperValue()); // 出错
原型链的问题
- 实例对象自身没有属性,通过方法修改 原型上的属性,其他实例对象也会被更改。
- 第二个问题 子类型在实例化 不能在父类型的构造函数传参。
盗用构造函数
基本思路: 在子类构造函数中调用父类构造函数。
用 applay() 和call() 方法以新创建的对象为上下文执行构造函数。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
SuperType.call(this)
}
let instancel = new SubType();
instancel.colors.push("blck");
console.log(instancel.colors);
let instance2 = new SubType();
console.log(instance2); // red blue green
// 通过调用父构造函数的 call 方法来实现继承,相当于新的SubType对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。
传递参数
盗用构造函数可以在子类构造函数中向父类构造函数传参。
function SuperType(name) {
this.name = name;
}
function SubType() {
SuperType.call(this, "Nicholas"); //传递参数
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Ni cholas"
console.log(instance.age); //29
盗用构造函数的问题
必须在构造函数中定义方法函数不能重用。
子类也不能访问父类原型上定义的方法。
所有类型只能使用构造函数模式。
组合继承
组合继承 综合了 原型链和盗用构造函数
基本思路,使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。
/ 组合继承
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;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
console.log(this.age);
}
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);
instance1.sayName(); // Nicholas
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // Greg
instance2.sayAge(); // 27
原型式继承
即使不自定义类型也可以通过原型实现对象之间的信息共享。
function object(o) {
function F() {
}
F.prototype = o
return new F()
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(person.friends);
let yetAnotherPerson = Object(person);
yetAnotherPerson.name = "linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);
ES5中通过增加Object.create()的方法将原型式继承的概念规范化了。
Object接收两个参数 作为新对象原型的对象,以及给新对象定义额外属性的对象。
在只有一个参数时,Object.create与这里的object方法相同。
object.create()的第二个参数与Object.defineProperties()的第二个参数一样:每个新增的属性都通过各自的描述符来描述。
这种添加方式 会遮蔽原型对象上的同名属性。
寄生式继承
与原型式继承比较接近的一种继承方式是寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
类
类声明:class Person {}
类表达式:const Animal = class { }
类跟函数的区别:函数受函数作用域限制,而类块作用域限制,
类跟函数表达式类似,类表达式在它们被求值钱也不能被引用,不过与函数定义不同的是,虽然函数声明可以提升 但是类定义不可以。
类的构成
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。
空的类照样有效,默认情况下类定义的代码都在严格模式下执行。
class Foo { };
// 有构造函数的类
class Bar {
constructor() { }
}
// 有获取函数的类,有效
class Baz {
get MyBaz() {
}
}
// 有静态方法的类
class Qux {
static myQux() { }
}
// 类表达式的名称是可选的,在把类表达式赋值给变量后,可以通过name属性取得类表达式的名称字符串,但不能在类表达式作用域外部访问这个表示符
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify() // PersonName PersonName
console.log(Person.name);
// console.log(PersonName);
类构造函数
constructor 关键字用于在类定义块内部创建类的构造函数。
方法名constructor 会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。
构造函数的定义不是必须的,不定义构造函数相当于将构造函数定义为空函数。
实例化
使用new操作符实例化Person的操作等于使用new调用其构造函数。唯一可感知的不同之处就是,javascript解释器知道使用new和类意味着应该使用constructor函数进行实例化。
使用new调用类的构造函数会执行如下操作。
- 在内存中创建一个新对象。
- 做个新对象内部的指针被赋值为构造函数的prototype属性。
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
- 执行构造函数内部的代码(给对象添加属性)。
- 如果构造函数返回空对象,则返回该对象;否则,返回刚创建的新对象。
类实例化时传入的参数会用作构造函数的参数,如果不需要参数,则类名括号后面的参数也是可选的。
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null
}
}
let p1 = new Person;
console.log(p1.name);
let p2 = new Person("hh");
console.log(p2.name);
默认情况下,类构造函数会在执行之后返回this对象,构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的this对象,那么这个对象会被销毁。
如果返回的不是this对象,而是其他对象,那么这个对象不会通过instanceof操作符检测出来跟类有关联,因为这个对象的原型指针并没有被修改。
class Person {
constructor(override) {
this.foo = "foo";
// 如果传递进来的参数 为true 则 改变返回值
if (override) {
return {
bar: "bar"
}
}
}
}
let p1 = new Person(),
p2 = new Person(true);
console.log(p1);
console.log(p1 instanceof Person);
console.log(p2);
console.log(p2 instanceof Person);
类构造函数与构造函数的主要区别是,调用构造函数必须使用new操作符。
而普通构造函数如果不使用new 调用,那么就会以全局的this通常是window作为内部对象。
调用类构造函数时如果忘记使用new 就会抛出错误。