Js面对对象
对象
1.对象基础
1.1创建对象
let person = new Object();
// person = {}; 等效 但其实不调用构造函数
person.name = "Nicholas";
person.age = 29;
//下面为字面量创建 与上面等价
let person = {
name: "Nicholas",
age: 29
};
注①:关于大括号({})
这里左大括号({)表示对象字面量开始 ,因为它处于表达式上下文中(赋值操作符期待一个值)。
如果({)出现在语句上下文比如 if 后,则表示语句块的开始。
注②:如果在 age: 29(最后一个属性)后面加( , )老版本浏览器可能报错
注③:数字属性会自动转换为字符串
1.2对象传参
function displayInfo(args) {
let output = "";
if (typeof args.name == "string"){
output += "Name: " + args.name + "\n";
}
if (typeof args.age == "number") {
output += "Age: " + args.age + "\n";
}
alert(output);
}
displayInfo({
name: "Nicholas",
age: 29
});
displayInfo({
name: "Greg"
});
1.3属性访问
程序中给出三种方法
console.log(person["name"]); // "Nicholas"
console.log(person.name); // "Nicholas" 首选
let propertyName = "name"; //适合属性中有空格的方法
console.log(person[propertyName]); // "Nicholas"
1.4属性的类型
属性分为两种:数据属性 和 访问器属性。
-
数据属性
包含一个存数据值的位置,有4个特性:
- [[Configurable]]:表示是否可以删除,重新定义,修改特性。
直接定义的属性默认为true。 - [[Enumerable]]:表示属性是否可以被for...in循环访问到。(默认true)
- [[Writable]]:表示属性值能否被修改。(默认true)
- [[Value]]:包含属性实际的值。(默认值为undefined)
如果要修改默认特性 必须使用 Object.defineProperty() 方法
接收三个参数:要添加属性的对象、属性名、描述对象(可以包含configurable、enumerable、writable和value属性)
let person = {}; Object.defineProperty(person, "name", { writable: false, value: "zhangsan" }); person.name = "lisi"; console.log(person.name); // "zhangsan" 无法修改了
注:省略的属性默认值为false
- [[Configurable]]:表示是否可以删除,重新定义,修改特性。
-
访问器属性
访问器属性不包含数据值。它们包含一个获取 (getter)函数和一个设置(setter)函数
- [[Get]]:获取函数,在读取属性时调用。(默认值为undefined)
- [[Set]]:设置函数,在写入属性时调用。(默认值为undefined)
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
只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。
-
Object.defineProperties() 定义多个属性
let book = {}; Object.defineProperties(book, { year_: { value: 2017 }, edition: { value: 1 } });
-
Object.getOwnPropertyDescriptor() 获取属性特性
返回值是一个对象,对于访问器属性包含configurable、enumerable、get和set属性,对于数据属性包含configurable、enumerable、writable和value属性。
let descriptor = Object.getOwnPropertyDescriptor(book, "year_"); console.log(descriptor.value); // 2017 console.log(descriptor.configurable); // false console.log(typeof descriptor.get); // "undefined"
ECMAScript 2017新增了 Object.getOwnPropertyDescriptors() 接收对象参数 返回以属性(自有属性)为键,描述对象为值的对象
console.log(Object.getOwnPropertyDescriptors(book)); // { // edition: { // configurable: false, // enumerable: false, // value: 1, // writable: false // }, // year: { // configurable: false, // enumerable: false, // get: f(), // set: f(newValue), // }, // ... // }
1.5对象的复制
Object.assign() 接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回true)属性 复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的 值,然后使用目标对象上的[[Set]]设置属性的值。
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: foo, b: bar }
注:访问器属性无法转移
1.6Object.is()
与 === 很像,但有些情况不同
// 正确的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
1.7对象语法糖
-
属性值简写
let name = 'zhangsan'; let person = { name: name }; // 等价 let person2 = { name };
注:代码压缩程序会正确处理,不必担心
-
可计算属性
const nameKey = 'name'; let person = {}; person[namekey] = 'zhangsan'; // 等价 let person = { [nameKey]: 'Matt' }; // 括号中为任意表达式
-
简写方法名
let person = { sayName: function(name) { console.log(`My name is ${name}`); } }; //简写后 let person = { sayName(name) { console.log(`My name is ${name}`); }, // set,get name_: '', get name() { return this.name_; }, set name(name) { this.name_ = name; } //兼容求值 [methodKey](name) { console.log(`My name is ${name}`); } };
1.8对象解构
ES6新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。
let person = {
name: 'Matt',
age: 27
};
// 1.普通解构
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
// 2.使用属性名
let { name, age, job } = person;
console.log(name); // Matt
console.log(age); // 27
console.log(job); // undefined 不存在属性默认值
// 3.解构时会把被解构变量当作对象
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError 这两个不是对象
let { _ } = undefined; // TypeError
// 4.先声明后解构,要加括号
let personName, personAge;
({name: personName, age: personAge} = person);
// 5.属性赋值
let personCopy = {};
({name: personCopy.name, age: personCopy.age} = person);
// 6.嵌套解构
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let { job: { title } } = person;
// 7.参数解构
function printPerson(foo, {name, age}, bar) {
console.log(arguments);
console.log(name, age);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
// 声明的变量可以使用,arguments中为一个对象
2.构造对象
2.1概述
虽然使用Object构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。
2.2工厂模式
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");
虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
2.3构造函数模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
};
}
要创建Person的实例,应使用new操作符。以这种方式调用构造函 数会执行如下操作。
- 在内存中创建一个新对象
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象(有return),则返回该对象;否则,返回刚创建的新对象。
2.3.1构造函数也是函数
任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数。
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到window对象
window.sayName(); // "Greg"
注:在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this始终指向Global对象(在浏览器中就是 window对象)。
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"
2.4.1理解原型
- 每个函数都有一个prototype属性(指向原型对象)
- 原型对象默认只有一个constructor属性回指构造函数,其他的所有方法都继承自Object。
- 实例对象有内部指针[[Prototype]]指向原型对象。
Firefox、Safari和Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。 - 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
(类型和原型链有关,构造函数只是赋值属性和类名(因为constructor指向它)有用,和对象没有太大关系) - 正常的原型链都会终止于Object的原型对象,Object原型的原型是null
// isPrototypeOf()调用对象是否为参数对象原型,继承自Object原型对象
console.log(Person.prototype.isPrototypeOf(person1)); // true
// Object.getPrototypeOf() 获得内部[[Prototype]]属性
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
// Object.setPrototypeOf() 为一个对象重新设置原型
// 不推荐使用
// Object.create() 以指定原型作为内部[[Prototype]]创建新对象
//相当于
function object(o){
function F(){}
F.prototype = o;
return new F();
}
let biped = {
numLegs: 2
};
let person = Object.create(biped);
console.log(person.numLegs); // 2
2.4.2原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索。
搜索开始于对象实例本身,发现则返回。
如果没有找到这个属性,则搜索会沿着指针进入原型对象,发现则返回。
如果还没有就进入原型的指针进入它的原型对象...直到找到。
注:原型上的值不能重写,如果重写则会遮蔽。除非delete属性否则遮蔽不会失效。
2.4.3一些方法
方法名 | 描述 |
---|---|
hasOwnProperty() | 判断属性在原型还是在实例上(true) |
Object.getOwnPropertyDescriptor() | 只能获取实例属性描述对象, 要获取原型上的描述需要传参为原型对象 |
in 操作符 | 如果可以访问则返回 true,不管在哪 |
for...in 循环 | 可以迭代出可枚举([[Enumerable]]为true)的属性 不管在哪 |
Object.keys() | 可枚举的实例属性,返回数组 |
Object.getOwnPropertyNames() Object.getOwnPropertySymbols() |
所有实例属性,返回数组 |
Object.values() | 返回对象值的数组 |
Object.entries() | 返回键/值对的数组 |
2.4.4自定义原型
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
如果自定义了原型对象,那么这个对象没有constructor属性指向Person,
虽然不会破坏 instanceOf 操作符识别类型,我们可以手动加入
Person.prototype = Person;
但要注意,以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性。而原生 constructor属性默认是不可枚举的。
// 恢复constructor属性
Object.defineProperty(Person.prototype,
"constructor", {
enumerable: false,
value: Person
});
2.4.5原型的不足
方法的共享是正确的,但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
3.继承
3.1原型链
原型链扩展了前面描述的原型搜索机制。在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型。
3.1.1默认原型
任何函数 的默认原型都是一个Object的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括toString()、valueOf() 在内的所有默认方法的原因。
3.1.2 原型判断
对原型链上的任何一个原型调用isPrototypeOf() 返回都是true(只要在原型链上)。
3.1.3关于方法
如果通过在自己原型上的同名方法覆盖父类原型上的方法。
3.1.4原型链的问题
由于目前是使用父类的对象作为子类的原型,所以父类对象上的属性就会变成共享的原型属性,这就是不会单独使用原型链的原因。
3.2盗用构造函数
基本思路是在子类的构造函数中使用apply(),call()方法调用父类的构造函数。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
//"red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors);
//"red,blue,green"
注:使用了call() 规定了被执行函数中this的值,相当于在子类对象上运行了父类对象的初始化代码。
传参
function SuperType(name){
this.name = name;
}
function SubType() {
// 继承SuperType并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
3.3组合继承
使用原型链继承父类的方法,使用盗用构造函数继承实例属性
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);
};
3.4原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
适合继承那些没有构造函数的对象(主要关注对象,而不在乎类型和构造函数)。
系统中提供了一个Object.create()方法,如果只传一个参数则与上方函数相同,还可以接收第二个参数
第二个参数与Object.defineProperties()中接收的参数对象相同,用于在返回对象上增加属性。
3.5寄生式继承
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
比原型继承多了增强步骤,寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。
3.6寄生式组合继承
组合继承存在的问题:
- 父类的构造函数分别在创建原型时和被盗用创建实例属性时调用了两次
- 父类的实例属性变为了原型属性,虽然会被遮蔽但是也占用了空间
解决方案
// 接收子类构造函数和父类构造函数
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype);
// 创建对象
prototype.constructor = subType;
// 增强对象
subType.prototype = prototype;
// 赋值对象
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
寄生式组合继承可以算是引用类型继承的最佳模式。
4.类
为了解决这些问题传统继承的各种问题,ECMAScript 6新引入的class关键字具有正式定义类的能力。
4.1类的定义
class Person{} //定义
const Animal = class{}; //表达式
//与函数不同都不能提升
//类的作用域为块作用域,函数为函数作用域 方法都在原型上
class Foo{
//构造函数
constructor(){}
//设置函数
get myBaz() {}
//静态方法
static myQuz(){}
}
//表达式定义名称 可以通过name属性访问 但不可以在外部通过它访问类
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
4.2构造
类与构造函数使用和创建对象的方式基本相同(类就是特殊的函数),不过只可以用new来创建
// 表达式也可以立即实例化
let p = new class Foo {
constructor(x) {
console.log(x);
}
}('bar'); // bar
4.3继承
可以通过原型链访问到类和原型上定义的方法。
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'); // bus, Bus {}
v.identifyPrototype('vehicle'); // vehicle, Vehicle {}
Bus.identifyClass('bus'); // bus, class Bus {}
4.1构造函数与super
类中定义 类构造函数、 实例方法和静态方法时可以使用super引用父类中的方法
-
调用静态方法
class Bus extends Vehicle { static identify() { super.identify(); } }
-
super不能单独使用,只能通过他调用构造函数或其他方法
-
super() 的作用就是创建一个内部对象并赋值给this,因此不能在super()前使用this。
-
如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返 回一个对象。
-
如果没有定义构造函数则会隐式调用super(),也可以传递参数
4.2抽象基类与new.target
new.target可以保存通过new关键字调用的类或函数,检测它的值就好。
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be
directly instantiated');
}
}
}
// 派生类
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directlyinstantiated
还可以在父类构造函数中检测要求的方法是否定义
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
class Bus extends Vehicle {
foo() {}
}
4.3继承原始类型
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 a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2)); //这里默认a2也会时SuperArray类型
// 开始时这样可以改变这个行为
class SuperArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
console.log(a2 instanceof SuperArray); // false