JavaScript高级程序设计 第八章---对象,类与面向对象编程
第八章(一)
内容
- 理解对象
- 理解对象创建过程
ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把ECMAScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。
8.1 理解对象
创建自定义对象的通常方式是创建Object 的一个新实例,然后再给它添加属性和方法,如下例所示:
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {console.log(this.name);
};
sayName()方法会显示this.name
的值,这个属性会解析为person.name
8.1.1 属性的类型
对象的属性通过一些内部特性来进行描述,。这些特性是由为JavaScript 实现引擎的规范定义的。因此,开发者不能在JavaScript 中直接访问这些特性了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]
。
属性分两种:数据属性和访问器属性。
1.数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。
在像前面例子中那样将属性显式添加到对象之后,[[Configurable]]、[[Enumerable]]和[[Writable]]都会被设置为true,而[[Value]]特性会被设置为指定的值。比如:
let person = {name: "Nicholas"
};
这里,我们创建了一个名为name 的属性,并给它赋予了一个值"Nicholas"。这意味着[[Value]]特性会被设置为"Nicholas",之后对这个值的任何修改都会保存这个位置。
要修改属性的默认特性,就必须使用Object.defineProperty()
方法。这个方法接收3 个参数:
- 要给其添加属性的对象
- 属性的名称
- 一个描述符对象
最后一个参数,即描述符对象上的属性可以包含:configurable、enumerable、writable 和value,跟相关特性的名称一一对应。根据要修改的特性,可以设置其中一个或多个值。比如:
let person = {};
Object.defineProperty(person, "name", {writable: false,value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
这个例子创建了一个名为name 的属性并给它赋予了一个只读的值"Nicholas"。这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。
此外,一个属性被定义为不可配置configurable: false
之后,就不能再变回可配置的了,再次调用Object.defineProperty()并修改任何非writable 属性会导致错误:
let person = {};
Object.defineProperty(person, "name", {configurable: false,value: "Nicholas"
});
// 抛出错误
Object.defineProperty(person, "name", {configurable: true,value: "Nicholas"
});
因此,虽然可以对同一个属性多次调用Object.defineProperty(),但在把configurable 设置为false 之后就会受限制了。
在调用Object.defineProperty()时,configurable、enumerable 和writable 的值如果不指定,则都默认为false。多数情况下,可能都不需要Object.defineProperty()提供的这些强大的设置,但要理解JavaScript 对象,就要理解这些概念。
2.访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter
)函数和一个设置(setter
)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。
访问器属性是不能直接定义的,必须使用Object.defineProperty()。下面是一个例子:
// 定义一个对象,包含伪私有成员year_和公共成员edition
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
year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。另一个属性year 被定义为一个访问器属性,其中获取函数简单地返回year_的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把year 属性修改为2018 会导致year_变成2018,edition 变成2。
即修改会进行传递。
这是访问器属性的典型使用场景,即设置一个属性值会导致一些其他变化发生。
在对象内部定义访问器属性:
dest = {set id(x) {console.log(x);}get id() {return 'foo';}
};
如上所示,定义了设置id属性和获取id属性的方法。
8.1.2 定义多个属性
ECMAScript 提供了Object.define-Properties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。
let book = {};
Object.defineProperties(book, {year_: {value: 2017},edition: {value: 1}
});
所有属性都是同时定义的,并且数据属性的configurable、enumerable 和writable 特性值都是false(使用Object.defineProperties这个方法定义就会默认为false,直接定义会默认为true)。
使用Object.getOwnPropertyDescriptor()
方法可以取得指定属性的属性描述符。
8.1.4 合并对象
ECMAScript 6 专门为合并对象提供了Object.assign()
方法。这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。
let dest, src, result;
/**
* 简单复制
*/
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
对于Object.assign(dest, src)
这个复制过程是先调用src的获取方法,获取属性后,调用dest的设置方法进行设置。
因此,Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。
**/**
* 对象引用
*/
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true**
通过最后一行的全等符合可以看出它们两个的a指向的是同一个对象,因此复制的只是对象的引用。
8.1.5 对象标识及相等判定
ECMAScript 6 规范新增了Object.is(),这个方法与===很像,但改善了一些特殊情况的判定。
// 这些情况在不同JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定NaN 的相等性,必须使用极为讨厌的isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true// 正确的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
8.1.6 增强的对象语法
1. 属性值简写
当属性名与变量名一样时:
let name = 'Matt';
let person = {name: name
};
console.log(person); // { name: 'Matt' }
可以使用简写:
let name = 'Matt';
let person = {name
};
console.log(person); // { name: 'Matt' }
如果没有找到同名变量,则会抛出ReferenceError。
2.可计算属性
有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为JavaScript 表达式而不是字符串来求值:
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); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
3.简写方法名
在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式,如下所示:
let person = {sayName: function(name) {console.log(`My name is ${name}`);}
};
person.sayName('Matt'); // My name is Matt
简写写法:
let person = {sayName(name) {console.log(`My name is ${name}`);}
};
person.sayName('Matt'); // My name is Matt
8.1.7 对象解构
可以在一条语句中使用嵌套数据实现一个或多个赋值操作。
简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。
通过匹配来获取值
下面的例子展示了两段等价的代码,首先是不使用对象解构的:
// 不使用对象解构
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); // Matt
console.log(personAge); // 27
使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。如果想让变量直接使用属性的名称,那么可以使用简写语法,比如:
let person = {name: 'Matt',age: 27
};
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
解构赋值不一定与对象的属性匹配,当引用的属性不存在时,会赋值undefined:
let person = {name: 'Matt',age: 27
};
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined
可以在解构赋值的同时定义默认值,若匹配成功会覆盖默认值:
let person = {name: 'Matt',age: 27
};
let { name='aa', job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer
当解构值的数量与对象的属性数量不等时,若值的数量小于属性数量会赋值前面的属性,若大于则会报错。
let person = {name: 'Matt',age: 27
};
let {name} = person;
console.log(name); // Matt{name, age, hair} = person; // Uncaught SyntaxError: Invalid or unexpected token
解构并不要求变量必须在解构表达式中声明。
不过,如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:
let personName, personAge;
let person = {name: 'Matt',age: 27
};({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27
解构在内部使用函数ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。
即会把上例中的person视为一个对象进行处理
这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据ToObject()的定义),null和undefined 不能被解构,否则会抛出错误(这两个类型不能被当成对象进行处理)。
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
1. 嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:
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' } }
在上例中,因为person的job属性是一个对象,所以赋值是通过引用赋值,所以当原本person里这个值发生变化时,copy的person对象中这个值也会发生变化。
8.2 创建对象
构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在ECMAScript 中区分构造函数和普通函数。
8.2.3 构造函数模式
使用自定义的构造函数来创建对象,例如:
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
具有以下特点
- 没有显式地创建对象。
- 属性和方法直接赋值给了this。
- 没有return。
要创建Person 的实例,应使用new 操作符。以这种方式调用构造函数会执行如下操作。
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype 属性。
- 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
Javascript中prototype属性详解
构造出来的对象的constructor属性会指向原函数:
p.constructor
/* ƒ Person(name, age) {this.name = name;this.age = age;*/
通过这种方式创造出的既是Object的实例又是Person的实例,因此可以体现出对象之间的差异,将对象标识为特定的一种类型。
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
1. 构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。任何函数只要使用new 操作符调用就是构造函数,而不使用new 操作符调用的函数就是普通函数。
若直接作为函数调用,会将这些属性添加到window对象:
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到window 对象
window.sayName(); // "Greg"
在调用一个函数而没有明确设置this 值的情况下(即没有作为对象的方法调用,或
者没有使用call()/apply()调用),this 始终指向Global 对象(在浏览器中就是window 对象)。因此在上面的调用之后,window 对象上就有了一个sayName()方法,调用它会返回"Greg"。
使用call()
函数调用,具体见:Function.prototype.call() 即通过call方法调用会将构造函数中的属性和方法添加到函数参数中指定的对象。
即会将person中的属性及方法添加到o中。
let o = new Object();
Person.call(o, "Kristen", 25);
// o.name
// "Kristen"
但它还会保持原有的对象标识,即不会变成构造函数生成的对象类型:
o instanceof Person;
// false
2. 构造函数的问题
构造函数创建的方法在每个实例上都创建一遍,类似于下面这种形式:
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 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");console.log(person1.sayName === person2.sayName); // true
如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。
8.2.4 原型模式
每个函数都会创建一个prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。
使用原型对象的好处:
在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型
function Person() {}Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {console.log(this.name);
};let person1 = new Person();
person1.sayName(); // "Nicholas"let person2 = new Person();
person2.sayName(); // "Nicholas"console.log(person1.sayName == person2.sayName); // true
如上所示,为原型添加初始值及方法。
构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。
与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此person1 和person2 访问的都是相同的属性和相同的sayName()函数。
1. 理解原型
P225页
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype 属性(指向原型对象)。原型对象只会默认获得一个名为constructor 的属性,指回与之关联的构造函数(相等于指向构造函数Person),并会默认继承来自于Object的方法(会继承一个属性和多个方法)
function Person() {}
/**
* 声明之后,构造函数就有了一个
* 与之关联的原型对象:
*/
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {// constructor: f Person(),
// __proto__: Object
console.log(Person.prototype.constructor === Person); // true
正常的原型链都会终止于Object 的原型对象
Object 原型的原型是null
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
构造函数,原型对象和对象实例是三个完全不同对象:
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
实例与构造函数没有直接联系,与原型对象有直接联系,实例通过__proto__
链接到原型对象,构造函数通过prototype
属性链接到原型对象
// 第一个表示对象p1的原型就是原型对象
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
原型对象并不是新类型的对象,只是Object类型的对象:
console.log(Person.prototype instanceof Person) // false
console.log(Person.prototype instanceof Object) // true
构造函数,实例对象和原型对象间的关系图
虽然生成的实例对象中没有属性和方法,但由于对象属性查找机制原型链的存在,会从原型对象上进行查找,这是能够正常访问对象属性和方法的原因
原型链
使用Object.getPrototypeOf()
可以方便地取得一个对象的原型,返回参数的内部特性[[Prototype]]
的值。例如:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
可以通过Object.create()
来创建一个新对象,同时为其指定原型:
let biped = {numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
2. 原型层级
即读取属性和方法时会按照原型链的顺序进行访问。虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。
在实例对象上添加原型对象中的同名属性,那就会在实例上创建这个属性并且会遮住原型对象上的属性,访问时会读取新的值。
只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。
在被遮蔽时,实例上调用hasPrototypeProperty()
返回false。
不过,使用delete
操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {console.log(this.name);
};let person1 = new Person();
let person2 = new Person();person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person2.name); // "Nicholas",来自原型delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
hasOwnProperty()
方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自Object的,会在属性存在于调用它的对象实例上时返回true 。
console.log(person1.hasOwnProperty("name")); // false
注意
ECMAScript 的Object.hasOwnPropertyDescriptor()
方法只对实例属性有
效。要取得原型属性的描述符,就必须直接在原型对象上调用Object.getOwnPropertyDescriptor()
。
如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。
3. 原型和in操作符
有两种方式使用in
操作符:单独使用和在for-in
循环中使用。在单独使用时,in 操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上。
即只要实例或原型对象有这个属性就会返回true:
console.log("name" in person1); // true
如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用hasOwnProperty()
和in 操作符:
function hasPrototypeProperty(object, name){return !object.hasOwnProperty(name) && (name in object);
}
使用Object.keys()
方法,可以获得对象上所有可枚举的实例属性。
这个方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组。
注意这里是实例属性,所以不会返回原型对象的属性。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {console.log(this.name);
};let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
constructor属性虽然也存在但不可枚举。使用Object.getOwnPropertyNames()
可以获取不可枚举的属性。
4. 属性枚举顺序
for-in 循环和Object.keys()的枚举顺序是不确定的,取决于JavaScript 引擎,可能因浏览器而异。
8.2.5 对象迭代
Object.values()
和Object.entries()
接收一个对象,返回它们内容的数组。Object.values()返回对象值的数组,Object.entries()返回键/值对的数组
const o = {foo: 'bar',baz: 1,qux: {}
};
console.log(Object.values(o));
// ["bar", 1, {}]
console.log(Object.entries((o)));
// [["foo", "bar"], ["baz", 1], ["qux", {}]]
注意,非字符串属性会被转换为字符串输出。另外,这两个方法执行对象的浅复制,即通过引用获取属性值为对象的属性,还是原来的对象。
console.log(Object.values(o)[0] === o.qux);
// true
console.log(Object.entries(o)[0][1] === o.qux);
// true
1. 其他类型方法
使用对象字面量来写原型对象(封装代码):
function Person() {}
Person.prototype = {name: "Nicholas",age: 29,job: "Software Engineer",sayName() {console.log(this.name);}
};
但这种写法相当于为Person.prototype这个属性赋了新的值,即重写了默认的原型对象。之前的写法是直接修改属性,所以不会影响。
这种写法会将一个新对象赋给Person.prototype,而不再是随着构造函数一起生成的对象,所以这个对象的constructor属性不再是Person函数,而是Object函数
let friend = new Person();console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true
如上所示,使用构造函数生成一个对象实例friend,它的属性继承于原型对象,也包括constructor属性,因此值是Object函数。
但可以通过使用其他方法来定义constructor 属性为构造函数。P235
2.原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对
象所做的修改也会在实例上反映出来。下面是一个例子:
let friend = new Person();
Person.prototype.sayHi = function() {console.log("hi");
};
friend.sayHi(); // "hi",没问题!
即使在实例定义之后修改原型对象也会在实例中反映出来
之所以会这样,主要原因是实例与原型之间松散的联系。在调用friend.sayHi()时,首先会从这个实例中搜索名为sayHi 的属性。在没有找到的情况下,运行时会继续搜索原型对象。因为实例和原型之间的链接就是简单的指针,而不是保存的副本,所以会在原型上找到sayHi 属性并返回这个属性保存的函数。
但是重写原型对象并不会反映在实例中,因为实例中指向原型对象的指针[[Prototype]]
是在实例生成时自动赋值的,因为重写原型对象并不会影响指针,所以实例中的方法和属性还是旧的原型对象上的。
function Person() {}
Person.prototype = {name: "Nicholas",age: 29,job: "Software Engineer",sayName() {console.log(this.name);}
};let friend = new Person();Person.prototype = {name: 'newName',sayName() {console.log(this.name);}
};friend.sayName(); // Nicholas
3. 原生对象原型
所有原生引用类型的构造函数(包括Object、Array、String 等)都在原型上定义了实例方法。
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。
比如,下面的代码就给String原始值包装类型的实例添加了一个startsWith()方法:
String.prototype.startsWith = function (text) {return this.indexOf(text) === 0;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
4. 原型的问题
原型模式弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
原型的最主要问题源自它的共享特性,当属性包含引用值时,在一个实例上进行修改会导致其他实例的该属性同样会被修改。
浅复制的问题
function Person() {}
Person.prototype = {constructor: Person,name: "Nicholas",age: 29,job: "Software Engineer",// 引用赋值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
原始值类型不会出现这些问题,因为在修改时相当于重写,然后就会产生覆盖并遮蔽。
JavaScript高级程序设计 第八章---对象,类与面向对象编程相关推荐
- 你是怎么看完《JavaScript权威指南》《JavaScript高级程序设计》等这类厚书的?
参考博客原址:https://www.cnblogs.com/tonykair/p/7502276.html 你是怎么看完<JavaScript权威指南><JavaScript高级程 ...
- Javascript高级程序设计3笔记 - 对象
完全是笔记,99.9%摘录自高程3,方便学习复习. 对象认识 "类" 原型 继承 一.对象认识 最原始的创建对象方式: 1 var person = new Object(); / ...
- 读javascript高级程序设计06-面向对象之继承
原型链是实现继承的主要方法,通过原型能让一个引用类型继承另一个引用类型. 1.原型链实现继承 function SuperType(){ this.superprop=1; } SuperType.p ...
- JavaScript高级程序设计读书笔记(第6章面向对象的程序设计之创建对象)
2019独角兽企业重金招聘Python工程师标准>>> 面向对象语言都有"类"的概念,而通过类可以创建任意多个具有相同属性和方法的对象. JS中没有"类 ...
- [Javascript 高级程序设计]学习心得记录9 js面向对象
感觉最难的部分就是面向对象了,大学期间学习的是面向过程的c/c++,工作之后也没有深入了解过面向对象,通过这次的学习和回顾,也算是对面向对象有了新的认识.不过,就我在书上学到了结合个人理解随便说说,很 ...
- 读javascript高级程序设计-目录
javascript高级编程读书笔记系列,也是本砖头书.感觉js是一种很好上手的语言,不过本书细细读来发现了很多之前不了解的细节,受益良多.<br/> 本笔记是为了方便日后查阅,仅作学习交 ...
- 《javascript高级程序设计》笔记:变量对象与预解析
上一篇:<javascript高级程序设计>笔记:内存与执行环境 上篇文章中说到: (1)当执行流进入函数时,对应的执行环境就会生成 (2)执行环境创建时会生成变量对象,确定作用域链,确定 ...
- 《Javascript高级程序设计》读书笔记之对象创建
<javascript高级程序设计>读过有两遍了,有些重要内容总是会忘记,写一下读书笔记备忘 创建对象 工厂模式 工厂模式优点:有了封装的概念,解决了创建多个相似对象的问题 缺点:没有解决 ...
- 《JavaScript高级程序设计》读书笔记 -12.1 window对象
<JavaScript高级程序设计>读书笔记 -12.1 window对象 12.1 window对象 12.1.1 Global作用域 12.1.2 窗口关系[不是很懂] 12.1.3 ...
最新文章
- swift 3.0 json解析、字典转模型三种方案
- linux进程及作业管理实验,Linux 进程及作业管理(示例代码)
- Mac下制作Ubuntu的启动U盘
- 为了追求极致的性能,Kafka掌控这11项要领
- 不到一秒卖出一部!荣耀9X系列国内销售29天破300万台
- UI素材|屏幕移动线框套件
- 挂载ntfs_NTFSTool for mac(NTFS硬盘读写工具) 中文免费版
- treeview子节点文本过长超过DIV自动换行CSS解决
- 英特尔主板快捷启动键_BIOS里如何设置U盘启动?硬盘怎么设置第一启动项?
- 互联网寒冬的思考,程序员该如何突破瓶颈?
- 屏幕种类有哪些,特点,怎么挑?TFT、LCD、OLED、IPS、TN的含义与区别?高色域、刷新率、广视角、雾面屏又是什么?
- 你一生中要听的一百首英文电影插曲
- 360极速浏览器、360浏览器如何清除缓存、清除历史记录、清除cookies
- python读取文件夹下所有txt_Python读取文件夹中TXT文档
- 数组过滤filter()方法
- ubuntu 16.04可以连接wifi,不能上网问题
- 利用Excel批量修改图片名称
- Appium:配置华为手机鸿蒙HarmonyOS系统参数
- Android 切换系统语言
- 黄仁勋骗过了全世界,三个多月都没人发觉!皮衣是假的厨房是假的,连他自己都是假的...
热门文章
- NCAR data-groundwater-CLM4.0
- unity3D 关于制作2D像素游戏需要用到的组件
- python如何输出16进制_python 以16进制打印输出的方法
- IDCNACCTBLN报表中显示不出来新增的科目的余额
- sql只返回一条数据 fetch frist 1 rowonly 跟rownum的区别
- /Z7、/Zi、/ZI(调试信息格式)
- 三、redis数据存储之跳跃表(SKIP LIST)
- hexo 安装 keep 主题
- 手机蹭网新体验之WiFi精灵
- 解决Map_server could not open -f问题