本文以《JavaScript高级程序设计》第4版作为基础参考,整理使用JavaScript开发过程中,对象数据使用相关的知识点。
本文是开发知识点系列第十二篇。
- 第一篇:JavaScript开发中变量、常量声明的规矩总结
- 第二篇:JavaScript开发:数据类型知识总结
- 第三篇:JavaScript开发:使用Number数据类型需要注意的问题
- 第四篇:JavaScript开发:操作符在实际开发中的使用总结
- 第五篇:JavaScript开发:流程控制语句在实际开发中的使用总结
- 第六篇:JavaScript开发:函数在实际开发中的使用总结(1)
- 第七篇:JavaScript开发:日期对象在开发中的使用总结
- 第八篇:JavaScript开发:正则表达式在开发中的使用总结
- 第九篇:JavaScript开发:函数在实际开发中的使用总结(2)
- 第十篇:JavaScript开发:字符串数据在开发中的使用总结
- 第十一篇:JavaScript开发:数组在开发中的使用总结
对象数据Object是一种复杂数据类型,是一种无序名值对的集合,它是一种无序的数据结构,其中的属性(键)和对应的值是成对存在的。
在JavaScript中,有一切皆对象之说,恰能说明对象的意义。同时对象是原型链的老二,老大是null;还是面向对象编程的主体。可见其重要性。
另外对象本身就是一种数据结构。
创建对象
在JavaScript中,创建对象的方法有很多种,一些常见的方法:
- 对象字面量:这是最简单的创建对象的方法,只需要在大括号中定义对象的属性和方法。
javascript
let obj = {
name: 'Alice',
age: 25,
sayHello: function() {
console.log('Hello, ' + this.name);
}
};
new
关键字:可以使用new
关键字和构造函数来创建对象。
javascript
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log('Hello, ' + this.name);
};
}
let alice = new Person('Alice', 25);
Object.create()
方法:可以使用Object.create()
方法创建一个新对象,并将现有的对象设置为新对象的原型。
javascript
let person = {
sayHello: function() {
console.log('Hello, ' + this.name);
}
};
let alice = Object.create(person);
alice.name = 'Alice';
alice.age = 25;
如果不想创建的对象有继承,可以使用
js
let alice = Object.create(null);
class
关键字:在ES6中,可以使用class
关键字定义类,然后使用new
关键字创建类的实例。
javascript
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log('Hello, ' + this.name);
}
}
let alice = new Person('Alice', 25);
创建对象的工厂模式
工厂模式是一种创建对象的设计模式,它提供一种创建对象的接口,由子类决定实例化哪一个类
javascript
function CarMaker() {}
CarMaker.prototype.drive = function() {
return "Vroom, I have " + this.doors + " doors";
};
CarMaker.factory = function(type) {
var constr = type,
newcar;
if (typeof CarMaker[constr] !== "function") {
throw {
name: "Error",
message: constr + " doesn't exist"
};
}
if (typeof CarMaker[constr].prototype.drive !== "function") {
CarMaker[constr].prototype = new CarMaker();
}
newcar = new CarMaker[constr]();
return newcar;
};
CarMaker.Compact = function() {
this.doors = 4;
};
CarMaker.Convertible = function() {
this.doors = 2;
};
CarMaker.SUV = function() {
this.doors = 24;
};
var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
console.log(corolla.drive()); // "Vroom, I have 4 doors"
console.log(solstice.drive()); // "Vroom, I have 2 doors"
console.log(cherokee.drive()); // "Vroom, I have 24 doors"
上面CarMaker
工厂可以创建三种类型的车:Compact
,Convertible
和SUV
。每种类型的车都有一个drive
方法,该方法返回一个字符串,表示车的类型和门的数量。
或者我想工厂模式就是工厂函数,将创建对象封装在函数中。可以批量创建更为复杂的对象类型。
对象构造函数的静态方法
Object构造函数有一些静态方法
- Object.assign(target, ...sources):用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象
javascript
let target = { a: 1 };
let source1 = { b: 2 };
let source2 = { c: 3 };
Object.assign(target, source1, source2);
console.log(target); // { a: 1, b: 2, c: 3 }
- Object.create(proto, [propertiesObject]):创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
javascript
let proto = { x: 10 };
let obj = Object.create(proto);
console.log(obj.x); // 10
- Object.defineProperty(obj, prop, descriptor):直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
javascript
let obj = {};
Object.defineProperty(obj, 'property1', {
value: 42,
writable: false
});
console.log(obj.property1); // 42
- Object.defineProperties(obj, props):直接在一个对象上定义新的属性或修改现有属性,并返回此对象
javascript
let obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: true
}
});
console.log(obj.property1); // true
console.log(obj.property2); // 'Hello'
- Object.entries(obj):返回一个给定对象自身可枚举属性的键值对数组
javascript
let obj = { foo: 'bar', baz: 42 };
console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]
- Object.freeze(obj):冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性
javascript
let obj = { prop: 42 };
Object.freeze(obj);
obj.prop = 33; // Throws an error in strict mode
console.log(obj.prop); // 42
- Object.getOwnPropertyDescriptor(obj, prop):返回指定对象上一个自有属性对应的属性描述符
javascript
let obj = { prop: 42 };
let descriptor = Object.getOwnPropertyDescriptor(obj, 'prop');
console.log(descriptor.value); // 42
- Object.getOwnPropertyNames(obj):返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性)组成的数组
javascript
let obj = { a: 1, b: 2, c: 3 };
console.log(Object.getOwnPropertyNames(obj)); // [ 'a', 'b', 'c' ]
- Object.getPrototypeOf(obj):返回指定对象的原型(内部[[Prototype]]属性的值)
javascript
let prototype = {};
let obj = Object.create(prototype);
console.log(Object.getPrototypeOf(obj) === prototype); // true
- Object.is(value1, value2):判断两个值是否是相同的值
javascript
console.log(Object.is('foo', 'foo')); // true
console.log(Object.is(window, window)); // true
console.log(Object.is('foo', 'bar')); // false
console.log(Object.is([], [])); // false
- Object.keys(obj):返回一个由一个给定对象的自身可枚举属性组成的数组
javascript
let obj = { a: 1, b: 2, c: 3 };
console.log(Object.keys(obj)); // [ 'a', 'b', 'c' ]
- Object.values(obj):返回一个给定对象自身的所有可枚举属性值的数组
javascript
let obj = { a: 1, b: 2, c: 3 };
console.log(Object.values(obj)); // [ 1, 2, 3 ]
对象构造函数静态属性
Object构造函数有一些静态属性
-
Object.prototype:表示Object的原型对象。
-
Object.length:属性的值为1,表示Object构造函数的参数个数。
-
Object.name:属性的值为"Object",表示构造函数的名称。
这些属性都是只读的,不能被修改。例如:
javascript
console.log(Object.prototype); // 输出:{...} (表示Object的原型对象)
console.log(Object.length); // 输出:1
console.log(Object.name); // 输出:"Object"
对象的原型链
有一张图清晰解释了原型链
原型prototype
实际是原型链上的一个节点。原型的constructor
指向创建该对象实例的构造函数。
Object.defineProperty
Object.defineProperty()
是Object的静态方法,它可以用来添加新属性或者修改对象的现有属性,并返回这个对象,该方法涉及到对象内部属性的修改,需要掌握
javascript
let obj = { name: 'John' };
Object.defineProperty(obj, 'name', {
value: 'Jane',
writable: false,
enumerable: true,
configurable: true
});
console.log(obj.name); // 输出: 'Jane'
上面使用 Object.defineProperty()
修改了 obj
对象的 name
属性。将 name
的值改为 'Jane'
,并设置 writable
属性为 false
,这就导致这个属性的值不能被改变。
enumerable
属性为 true
,这就导致这个属性会出现在对象的枚举属性中。configurable
属性为 true
,这就导致这个属性的描述符可以被改变,也可以从对象中删除这个属性。
如果尝试改变 name
属性的值,由于 writable
属性被设置为 false
,这个值不会被改变:
javascript
obj.name = 'Jack';
console.log(obj.name); // 输出: 'Jane'
Proxy
Proxy是ES6引入的新特性,它用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
Proxy通过两个参数进行初始化:目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)和处理程序对象(一个对象,其属性是当在代理对象上执行一个操作时定义的函数)。
一个简单的Proxy使用
javascript
let target = {};
let handler = {
get: function(target, prop, receiver) {
console.log(`Getting ${prop}`);
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
let proxy = new Proxy(target, handler);
proxy.name = 'John'; // 输出: Setting name to John
console.log(proxy.name); // 输出: Getting name, John
上面定义了一个处理程序对象,它有两个方法:get
和set
。get
方法在读取代理对象的属性时被调用,set
方法在设置代理对象的属性时被调用。使用这个处理程序对象和一个空目标对象来创建一个新的Proxy对象。
当设置proxy对象的name
属性时,set
方法被调用,输出"Setting name to John"
。当读取proxy对象的name
属性时,get
方法被调用,输出"Getting name"
和"John"
。
Proxy之于Object对象提供了一种强大的方式来监视和干预JavaScript的基本操作。
vue3使用Proxy替代Object.defineProperty
很多人都知道vue3源码使用Proxy替代了vue2源码的Object.defineProperty
,这带来什么好处?这也是面试常考的一道题
-
检测新增属性:在Vue 2中,由于
Object.defineProperty
的限制,无法检测到对象属性的添加或删除。而Proxy
可以解决这个问题,它可以拦截对象的任何改变,包括新增或删除属性。 -
数组变更检测:Vue 2中,数组的某些方法(如
push
、pop
等)被单独处理,以便检测数组的变化。而Proxy
可以直接监听数组的变化,无需特殊处理。 -
性能优化:
Proxy
相比Object.defineProperty
有更好的性能,因为它不需要递归遍历所有属性将其转化为getter/setter。 -
代码简化:由于
Proxy
的强大拦截能力,Vue 3的源码相比Vue 2更加简洁。
但是,Proxy
是ES6的新特性,不被IE浏览器支持。这也是Vue 3不再支持IE的一个原因。
对象实例的静态属性和静态方法
class模式
静态属性和静态方法是直接添加到构造函数上,而不是添加到实例对象上的。这意味着不能通过一个类的实例来访问静态属性和静态方法,而是需要直接通过类来访问。
一个例子
javascript
class MyClass {
static myStaticProperty = 'This is a static property';
static myStaticMethod() {
return 'This is a static method';
}
}
console.log(MyClass.myStaticProperty); // 输出: 'This is a static property'
console.log(MyClass.myStaticMethod()); // 输出: 'This is a static method'
上面myStaticProperty
是一个静态属性,myStaticMethod
是一个静态方法。可以通过MyClass.myStaticProperty
和MyClass.myStaticMethod()
来访问它们。
不能通过一个类的实例来访问静态属性和静态方法
javascript
let myInstance = new MyClass();
console.log(myInstance.myStaticProperty); // 输出: undefined
console.log(myInstance.myStaticMethod()); // 抛出TypeError
new模式
一个例子
javascript
function MyClass() {}
MyClass.myStaticProperty = 'This is a static property';
MyClass.myStaticMethod = function() {
return 'This is a static method';
};
console.log(MyClass.myStaticProperty); // 输出: 'This is a static property'
console.log(MyClass.myStaticMethod()); // 输出: 'This is a static method'
上面myStaticProperty
是一个静态属性,myStaticMethod
是一个静态方法。可以通过MyClass.myStaticProperty
和MyClass.myStaticMethod()
来访问它们。
对象的继承
组合式继承
组合式继承(Combination Inheritance)是JavaScript中最常用的继承模式,它结合了原型链继承和借用构造函数继承的优点。
一个简单例子
javascript
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
}
function Child(name, age) {
Parent.call(this, name); // 借用构造函数继承
this.age = age;
}
Child.prototype = new Parent(); // 原型链继承
Child.prototype.constructor = Child; // 修复构造函数引用
Child.prototype.getAge = function() {
return this.age;
}
var child1 = new Child('Tom', 20);
child1.colors.push('black');
console.log(child1.getName()); // 输出:Tom
console.log(child1.getAge()); // 输出:20
console.log(child1.colors); // 输出:['red', 'blue', 'green', 'black']
var child2 = new Child('Jerry', 22);
console.log(child2.getName()); // 输出:Jerry
console.log(child2.getAge()); // 输出:22
console.log(child2.colors); // 输出:['red', 'blue', 'green']
上面Parent
是父类,Child
是子类。首先在Child
的构造函数中调用Parent.call(this, name);
来借用Parent
的构造函数,这样Child
就可以继承Parent
的实例属性。
然后将Child
的原型设置为Parent
的一个新实例,这样Child
就可以继承Parent
的原型方法。最后,修复了Child
的构造函数引用,使其指向Child
自身。
这样,Child
就可以继承Parent
的实例属性和原型方法,而且实例属性不会被所有实例共享,每个实例都有自己的一份拷贝。
原型式继承
原型式继承是通过创建一个新对象,然后将这个新对象的原型设置为另一个对象,从而实现继承。
在ES5中,可以使用Object.create
方法来实现原型式继承。一个简单的例子
javascript
var person = {
name: 'Default',
getName: function() {
return this.name;
}
};
var john = Object.create(person);
john.name = 'John';
console.log(john.getName()); // 输出:John
var jane = Object.create(person);
jane.name = 'Jane';
console.log(jane.getName()); // 输出:Jane
上面person
是一个对象,它有一个name
属性和一个getName
方法。使用Object.create(person)
创建了一个新对象,并将这个新对象的原型设置为person
。这样,新对象就可以继承person
的所有属性和方法。
不使用Object.create
,可以创建一个函数来模拟它
javascript
function createObject(proto) {
function F() {}
F.prototype = proto;
return new F();
}
var person = {
name: "default",
getName: function() {
return this.name;
}
};
var john = createObject(person);
john.name = "John";
console.log(john.getName()); // 输出:"John"
var jane = createObject(person);
jane.name = "Jane";
console.log(jane.getName()); // 输出:"Jane"
原型式继承的一个缺点,如果原型对象的属性是引用类型,那么所有实例都会共享这个属性。这是因为这些实例实际上是共享同一个原型对象,所以它们也共享同一个引用类型的属性。
寄生组合式继承
寄生组合式继承是一种改良的继承模式,它结合了组合式继承和寄生式继承的优点,避免了组合式继承的缺点(即调用两次父类构造函数)。
一个简单例子
javascript
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
}
function Child(name, age) {
Parent.call(this, name); // 借用构造函数继承
this.age = age;
}
// 寄生组合式继承
(function() {
var Super = function() {};
Super.prototype = Parent.prototype;
Child.prototype = new Super();
})();
Child.prototype.constructor = Child; // 修复构造函数引用
Child.prototype.getAge = function() {
return this.age;
}
var child1 = new Child('Tom', 20);
child1.colors.push('black');
console.log(child1.getName()); // 输出:Tom
console.log(child1.getAge()); // 输出:20
console.log(child1.colors); // 输出:['red', 'blue', 'green', 'black']
var child2 = new Child('Jerry', 22);
console.log(child2.getName()); // 输出:Jerry
console.log(child2.getAge()); // 输出:22
console.log(child2.colors); // 输出:['red', 'blue', 'green']
上面首先在Child
的构造函数中调用Parent.call(this, name);
来借用Parent
的构造函数,这样Child
就可以继承Parent
的实例属性。然后,创建了一个空的构造函数Super
,并将它的原型设置为Parent
的原型,然后将Child
的原型设置为Super
的一个新实例。这样Child
就可以继承Parent
的原型方法,而且不会调用Parent
的构造函数。
最后,修复Child
的构造函数引用,使其指向Child
自身。这样Child
就可以继承Parent
的实例属性和原型方法,而且实例属性不会被所有实例共享,每个实例都有自己的一份拷贝。同时,只调用了一次Parent
的构造函数,避免了不必要的性能开销。
class继承
ES6中引入了class
关键字,使得实现继承变得更加简单和直观。一个简单例子
javascript
class Parent {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}
getAge() {
return this.age;
}
}
let child = new Child('Tom', 20);
console.log(child.getName()); // 输出:Tom
console.log(child.getAge()); // 输出:20
上面Parent
是一个类,它有一个构造函数和一个getName
方法。Child
是一个继承自Parent
的类,它也有一个构造函数和一个getAge
方法。在Child
的构造函数中,使用super
关键字来调用父类的构造函数。
如果子类中定义了构造函数,那么在构造函数中必须先调用super
。这是因为子类的this
对象在调用super
之前是无法使用的,因为它需要通过父类的构造函数来初始化。
Object和map的区别
Map 和 Object 都是 JavaScript 中用于存储键值对的数据结构,它们的区别:
-
键的类型:在 Object 中,键只能是字符串或者 Symbol。但在 Map 中,键可以是任何类型,包括函数、对象和基本类型。
-
插入顺序:在 Map 中,键值对是按照插入顺序排序的。所以当对 Map 进行迭代时,键值对会按照它们被添加到 Map 的顺序返回。但在 Object 中,键值对的顺序是不确定的。
-
性能:在频繁增加和删除键值对的场景下,Map 通常会有更好的性能。
-
遍历:Map 有很多方便的方法可以用于遍历,如 keys()、values() 和 entries(),以及 forEach 方法。而 Object 则没有这些内置的遍历方法。
-
大小:可以很容易地获取到 Map 的大小(键值对的数量),只需使用 Map 的 size 属性即可。而要获取 Object 的大小,则需要手动计算。
一个 Map 例子
javascript
let map = new Map();
map.set('name', 'John');
map.set(1, 'number one');
map.set({}, 'an object');
console.log(map.get('name')); // 输出: 'John'
console.log(map.get(1)); // 输出: 'number one'
console.log(map.size); // 输出: 3
for (let [key, value] of map) {
console.log(key, value);
}
上面创建了一个 Map,并添加了一些键值对。可以看到,Map 可以接受任何类型的键,包括对象。还可以很容易地获取到 Map 的大小,以及遍历 Map 中的所有键值对。
Object的this指向
Object的this
指向主要取决于函数的调用方式,具体环境具体分析。一些常见的情况
- 在全局作用域或函数内部调用函数,
this
指向全局对象(在浏览器中是window
):
javascript
function test() {
console.log(this);
}
test(); // 输出:Window
严格模式下为undefined
。
- 作为对象的方法调用,
this
指向该对象:
javascript
var obj = {
name: 'Tom',
sayHello: function() {
console.log(this.name);
}
};
obj.sayHello(); // 输出:Tom
- 使用构造函数调用,
this
指向新创建的对象:
javascript
function Person(name) {
this.name = name;
}
var tom = new Person('Tom');
console.log(tom.name); // 输出:Tom
- 使用
call
、apply
或bind
调用,this
指向指定的对象:
javascript
function sayHello() {
console.log(this.name);
}
var obj = {name: 'Tom'};
sayHello.call(obj); // 输出:Tom
另外箭头函数没有自己的this
,箭头函数中的this
是继承自外层代码块的。
javascript
var obj = {
name: 'Tom',
sayHello: function() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
obj.sayHello(); // 输出:Tom
上面setTimeout
中的箭头函数继承了sayHello
方法中的this
,所以this.name
输出的是Tom
。
判断一个对象为{}
判断一个对象为{}看起来简单,实际并不简单,考察了对对象内置属性的理解对常见对象处理方法的理解。
我之前写过写过一篇文章专门分析判断一个对象为{}:一道面试题:怎么判断一个对象是空对象{}
对象的深度优先遍历和广度优先遍历
这也是一道面试题。你是否能够一下判断出for in
结合递归的思路是对象深度优先遍历还是广度优先遍历?
js
var obj = {
a: {
b: {
d: {
f: 22
}
},
c: {
e: {
g: 33
}
}
},
x: {
y: 44
}
}
function selfMap(obj){
for(let key in obj) {
console.log(key)
if(typeof obj[key] === 'object') {
selfMap(obj[key])
}
}
}
selfMap(obj)
for in
结合递归是深度优先遍历。所以后面的深度优先遍历主逻辑也是递归。
深度优先遍历
js
var obj = {
a: {
b: {
d: {
f: 22
}
},
c: {
e: {
g: 33
}
}
},
x: {
y: 44
}
}
const DFSARR = []
// Depth First Search
function DFS (obj) {
if (obj === null || typeof obj !== 'object') return
Object.entries(obj).map(([k, v], index) => {
DFSARR.push(k)
if (typeof v === 'object') {
DFS(v)
}
})
return DFSARR
}
console.log(DFS(obj)) // ['a', 'b', 'd', 'f', 'c', 'e', 'g', 'x', 'y']
广度优先遍历
广度优先遍历原理利用Object.entries
的原理:其可以将对象转为对象数组,且是第一层键值对的数组。
js
var obj = {
a: {
b: {
d: {
f: 22
}
},
c: {
e: {
g: 33
}
}
},
x: {
y: 44
}
}
console.log(Object.entries(obj))
之后,只要继续按照这个逻辑处理下去就可以了,就是广度优先遍历。
js
var obj = {
a: {
b: {
d: {
f: 22
}
},
c: {
e: {
g: 33
}
}
},
x: {
y: 44
}
}
const BFSARR = []
// Breadth First Search
function BFS (obj) {
const undo = []
if (obj === null || typeof obj !== 'object') return
undo.unshift(obj)
while (undo.length) {
const item = undo.shift()
Object.entries(item).map(([key, val]) => {
BFSARR.push(key)
undo.push(val)
})
};
return BFSARR
}
console.log(BFS(obj)) // ['a', 'x', 'b', 'c', 'y', 'd', 'e', 'f', 'g']
面向对象编程
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它使用"对象"来设计软件和创建可重用的代码。在OOP中,每个对象都是一个特定类别的实例,类别(类)定义了对象的属性(数据)和方法(操作数据的函数)。
面向对象编程的主要特点包括:
- 封装:封装是指将对象的状态(属性)和行为(方法)包装在一起,并对对象的内部状态进行隐藏,只通过公开的方法来访问和修改。
实现对象属性封装
javascript
function Person(name, age) {
var _name = name;
var _age = age;
this.getName = function() {
return _name;
}
this.getAge = function() {
return _age;
}
this.setName = function(name) {
_name = name;
}
this.setAge = function(age) {
_age = age;
}
}
var person = new Person('Tom', 20);
console.log(person.getName()); // 输出:Tom
console.log(person.getAge()); // 输出:20
person.setName('Jerry');
person.setAge(22);
console.log(person.getName()); // 输出:Jerry
console.log(person.getAge()); // 输出:22
上面Person
是一个构造函数,它有两个私有变量_name
和_age
,以及四个公有方法getName
、getAge
、setName
和setAge
。
这四个公有方法构成了Person
的公有接口,它们可以访问和修改私有变量_name
和_age
。但是,私有变量_name
和_age
本身是无法直接访问的,它们被封装在Person
的内部。
-
继承:继承是一种创建新类的方式,新创建的类(子类)可以继承现有类(父类)的属性和方法,并可以添加新的属性和方法或覆盖父类的方法。上面已经过继承这里就举例了。
-
多态:多态是指允许使用一个接口表示多种形态的对象。在运行时,可以根据实际的对象类型来调用相应的方法。
实现面向对象编程的多态
javascript
function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function() {
console.log('The animal makes sound');
}
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.makeSound = function() {
console.log('The dog barks');
}
function Cat(name) {
Animal.call(this, name);
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.makeSound = function() {
console.log('The cat meows');
}
var dog = new Dog('Tom');
dog.makeSound(); // 输出:The dog barks
var cat = new Cat('Jerry');
cat.makeSound(); // 输出:The cat meows
上面中Animal
是一个基类,它有一个makeSound
方法。Dog
和Cat
是两个派生类,它们都继承自Animal
,并且都重写了makeSound
方法。
当调用dog.makeSound()
和cat.makeSound()
时,它们会调用各自的makeSound
方法,而不是Animal
的makeSound
方法。
可以使用同一个接口(makeSound
方法)来表示不同的行为(狗叫和猫叫),这就是多态的核心思想。
一个使用JavaScript的面向对象编程例子
javascript
// 定义一个构造函数
function Dog(name) {
this.name = name; // 属性
}
// 在原型上定义方法
Dog.prototype.bark = function() {
console.log(this.name + " says: Woof!");
};
// 使用构造函数创建对象
var myDog = new Dog("Rover");
// 调用对象的方法
myDog.bark();
上面Dog
是一个构造函数,它定义了一个属性name
。在Dog
的原型上定义了一个方法bark
。然后使用new
关键字和Dog
构造函数创建了一个新的对象myDog
,并调用了它的bark
方法。
在ES6中,JavaScript引入了class
关键字,使得面向对象编程更加接近于其他面向对象语言。以下是使用class
的版本:
javascript
// 定义一个类
class Dog {
constructor(name) {
this.name = name; // 属性
}
// 方法
bark() {
console.log(this.name + " says: Woof!");
}
}
// 使用类创建对象
var myDog = new Dog("Rover");
// 调用对象的方法
myDog.bark();
上面Dog
是一个类,它有一个构造函数和一个方法bark
。使用new
关键字和Dog
类创建了一个新的对象myDog
,并调用了它的bark
方法。
面向对象编程(Object-Oriented Programming,OOP)作为一种编程范式,它使用"对象"来设计软件和创建可重用的代码,在现代开发中被大量应用,它的优点
-
模块化:面向对象编程鼓励将程序分解为一系列独立的对象,每个对象都有自己的职责。这种模块化可以使代码更易于理解、维护和修改。
-
重用性:通过继承,子类可以重用父类的代码,这可以减少代码的冗余,提高代码的重用性。
-
封装:对象可以将其内部状态和行为封装起来,只暴露出必要的接口。这可以隐藏内部实现细节,提高代码的安全性。
-
多态:面向对象编程允许对象根据当前状态或数据来改变其行为。它可以使代码更灵活,更易于扩展。
总结要点
- 创建对象可以使用字面量、使用new关键和
Object.create()
,并且可以使用Object.create()
创建没有继承的对象。 - 创建对象的工厂模式或者工厂函数可以批量创建更为复杂的对象类型。
- 对象构造函数有不少静态方法:
Object.assign(target, ...sources)
、Object.create(proto, [propertiesObject])
、Object.defineProperty(obj, prop, descriptor)
、Object.freeze(obj)
、Object.keys(obj)
、Object.values(obj)
、Object.entries(obj)
等等。 - 对象构造函数有一些静态属性:
Object.prototype
、Object.length
、Object.name
。 - 对象的原型链:原型
prototype
实际是原型链上的一个节点。原型的constructor
指向创建该对象实例的构造函数。 - Proxy和Object.defineProperty()对比:Proxy可以拦截对象的任何改变,包括新增或删除属性;Proxy可以直接监听数组的变化,无需特殊处理;Proxy相比Object.defineProperty有更好的性能,因为它不需要递归遍历所有属性将其转化为getter/setter。
- class方式的类创建静态属性需要使用关键字static,原型模式创建静态属性或者方法直接挂载在构造函数上。
- 原型模式的对象继承最好采用寄生组合式继承。
- 在 Object 中,键只能是字符串或者 Symbol。但在 Map 中,键可以是任何类型,包括函数、对象和基本类型;在 Map 中,键值对是按照插入顺序排序的。所以当对 Map 进行迭代时,键值对会按照它们被添加到 Map 的顺序返回。但在 Object 中,键值对的顺序是不确定的。
- Object的
this
指向主要取决于函数的调用方式,具体环境具体分析。 - 判断一个对象为{}看起来简单,实际并不简单。
- 对象的深度遍历使用的是递归,对象的广度遍历使用的是数据结构:队列。
- 学会使用面向对象编程。
本文完。