面向对象编程包含哪些重要性质?封装、继承、多态
如何理解面向对象编程?
举例来说,用来表示一个单词或者短语的一串字符通常被称为字符串。
所有字符串都是String类的一个实例
相比其他语言(如Java、python等传统OOP语言),JavaScript的机制和类完全不同。
JavaScript原来是没有class关键字的(es6新增的class其实也是语法糖),一般的,我们会通过类似构造函数 + new 的方法来新建对象(但其实并非构造函数)
function Foo(name) {
this.name = name;
this.myName = function(){
return this.name
}
}
let foo1 = new Foo('Alice');
foo.show();
这种做法会有问题:
function Foo(name) {
this.name = name;
this.myName = function(){
return this.name
}
}
let foo1 = new Foo('Alice');
let foo2 = new Foo('Bob');
console.log(foo1.myName === foo2.myName); // false 为什么?
两个对象的show方法是不同的,理论上这俩应该是相同的,毕竟都是继承自类,比如python中我们会有:
class Foo:
def __init__(self, name):
self.name = name
def myName(self):
return self.name
foo1 = Foo('Alice')
foo2 = Foo('Bob')
print(id(foo1.myName) == id(foo2.myName)) # 输出:False
print(id(foo1.myName.__func__) == id(foo2.myName.__func__)) # 输出:True
为什么id得到的结果会不相等?foo1.myName(通过实例去访问方法时),Python 实际上会创建一个绑定方法(bound method)对象。这个对象将方法(在这个例子中是 Foo.myName)和特定的实例(在这个例子中是 foo1)绑定在一起。
实际上,在像 Python 这样的面向对象语言中,方法并不是每个实例的独立副本。当你创建一个对象(实例化一个类)时,实例并不会拷贝类的方法。相反,所有的实例都会共享同样的方法。
然后上文中我们的实现存在问题:
function Foo(name) {
this.name = name;
this.myName = function(){
return this.name
}
}
let foo1 = new Foo('Alice');
let foo2 = new Foo('Bob');
console.log(foo1.myName === foo2.myName); // false 为什么?
也就是占了两片内存,这样每次新建一个对象,都会为show函数新开辟一段内存,浪费空间。
为了让两个对象上的 show
方法是同一个,我们可以利用函数的 prototype
属性(原型对象,后文会提到),这是每个函数都具有的属性。
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
let foo1 = new Foo('Alice');
let foo2 = new Foo('Bob');
console.log(foo1.myName === foo2.myName); // true
这样每次新new出来的对象,它们的myName方法都是指向同一片内存。
但是foo1和foo2本身是没有myName方法的,它们都只有一个name属性。
不信我们将foo1打印出来看看(注意不同浏览器结果不一样)
JavaScript 中的 Prototype 是每个对象内部的一个隐藏属性,它是对另一个对象的引用,被称为这个对象的“原型”。这个原型对象本身也可能有它自己的原型,这样一层层向上直到一个对象的原型为 null
,形成一条原型链。这种机制使得对象可以继承其他对象的属性和方法。
[[Prototype]]
属性在日常编程中通常不直接访问。在大多数现代浏览器中,可以使用 __proto__
属性访问它,但这并不推荐,因为它不是所有环境都支持的标准属性。更常见的做法是使用 Object.getPrototypeOf(obj)
函数来获取一个对象的原型,或使用 Object.create(proto)
来创建一个新对象,同时设置其 [[Prototype]]
。
JavaScript 的许多内置方法,比如 toString(),valueOf() 等,都是定义在内置对象的 prototype 属性上的,例如 Object.prototype,Array.prototype。当我们创建一个新的对象或数组时,这些方法会通过原型链被新对象继承,因此我们可以调用 obj.toString() 或 arr.length 等。
当我们访问一个对象的属性时,JavaScript 首先会在该对象自身的属性中查找。如果没有找到,它会沿着原型链去查找,直到找到属性或者到达原型链的末端。
简单来说,[[Prototype]]
其实就是一个特殊的属性,一般通过 __proto__
访问,可以和其他对象的 [[Prototype]]
进行关联,形成一条链
Object.create 会创建一个对象并把这个对象的[[Prototype]]
关联到指定的对象。
Object.create(null) 是一个常见用法,会创建一个拥有空(或者说null)[[Prototype]]
链接的对象,适合保存数据。
var anotherObject = {
a:2
};
// 创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
myObject.a; // 2 为什么能找到?
我们可以理解为, myObject 的 [[Prototype]]
与 anotherObject 的 [[Prototype]]
关联起来了,形成了一条链
现在myObject对象的 Prototype 关联到了anotherObject。显然myObject.a并不存在,但是尽管如此,属性访问仍然成功地(在anotherObject中)找到了值2。但是,如果anotherObject中也找不到a并且 Prototype 链不为空的话,就会继续查找下去。
尽头在哪里?
所有普通的Prototype链最终都会指向内置的Object.prototype。由于所有的“普通”对象都“源于”(或者说把Prototype链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。
理解如下代码:
var anotherObject = {
a:2
};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("a"); // false
myObject.a++; // 关注这里!
anotherObject.a; // 输出什么?
myObject.a; // 输出什么?
myObject.hasOwnProperty("a"); // true
思考结果
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty("a"); // true
myObject.a++看起来应该查找并增加anotherObject.a属性,但是别忘了++操作相当于 myObject.a = myObject.a + 1
。
++操作首先会通过 [[Prototype]]
查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用将值3赋给myObject中新建的屏蔽属性a
在 JavaScript 中,每一个函数都有一个 prototype 属性,这个属性是一个对象,称为“原型对象”。这个原型对象有一个特殊的用途:当一个函数被用作构造函数来创建新的对象时,这个新对象的内部 Prototype 属性就会被链接到构造函数的 prototype 对象。这就是说,新对象会继承原型对象的属性和方法。
原型对象和 Prototype 之间的主要联系:原型对象定义了一个模板,新对象通过其内部的 Prototype 属性继承了这个模板。
为何需要这种机制?
这是因为 JavaScript 是一种基于原型的语言,它使用原型链来实现继承。原型对象和 Prototype 的机制使得所有的对象实例可以共享相同的属性和方法,这样不仅可以节省内存,也使得在对象实例上的操作可以反映到所有的实例上。
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; // true
JavaScript 的 new
关键字用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。new
关键字执行以下步骤:
new
关键字首先会创建一个新的空对象。 [[Prototype]]
属性会被链接到构造函数的 prototype
对象。新对象可以访问构造函数原型上的属性和方法。 new
后面调用的函数)被执行。构造函数内部的 this
关键字会被指向新创建的对象。如果构造函数返回非空对象,则返回该对象,否则返回刚刚新创建的对象。 new
表达式将返回新创建的对象。 new 会依次执行下面的步骤
伪代码如下
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
// 将 arguments 对象转为数组
var args = [].slice.call(arguments);
// 取出构造函数
var constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context = Object.create(constructor.prototype);
// 执行构造函数
var result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
return (typeof result === 'object' && result != null) ? result : context;
}
// 实例
var actor = _new(Person, '张三', 28);
由此我们总结出
[[Prototype]]
属性,称为对象的原型。[[Prototype]]
属性继承了这个模版注意到,原型对象还有一个 constructor
, 这个 constructor 指回原来的函数。
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
constructor 属性有什么用处呢?一般的,我们可以用 constructor 属性来判断一个实例对象是由哪个构造函数创建的。同时,如果我们想要复制一个对象,创建一个新的实例,我们也可以通过 constructor 属性来实现。
从上一节可以看到,所有类对象在实例化的时候将会拥有原型对象中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
console.log(cat1.color) // 'white'
console.log(cat2.color) // 'white'
color
的属性查找过程如下:
color
属性cat1.__proto__
(也就是 Animal.prototype
) 中寻找 colorcat1.__proto__.__proto__
中寻找 colorObject.prototype
,而 Object.prototype.__proto__
为 null
原型对象的属性不是实例自身的属性,只要修改原型对象,变动就立刻体现在所有实例对象上
Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"
但是实际上原型链到这里还没有结束,因为我们忽略了一点,函数也是一个对象,它也有自己的 __proto__
属性,但是这个属性指向哪里呢?
先看一下 JavaScript 的 function Function
let my_func = new Function("a", "b", "return a+b;");
my_func(1,2) //3
可以通过 Function
来创建一个函数对象,意味着所有的函数对象,都是通过 Function
生成的
即所有的 function
都是通过 Function
来生成的
所以最终的原型链图如下
总结:
__proto__
属性,该属性对应该对象的原型.__proto__
属性.constructor
属性,该属性对应创建所有指向该原型的实例的构造函数.prototype
和 constructor
属性进行相互关联.三种对象的关系:
prototype
属性定义的对象,它定义了所有通过特定构造函数创建的对象实例所共享的属性和方法。 **[[Prototype]]**
:这是对象的一个内部属性,指向该对象的原型对象。当我们尝试访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 会沿着 [[Prototype]]
链接去原型对象上查找。 **constructor**
:这是原型对象的一个属性,指向与该原型关联的构造函数。实例对象通过 [[Prototype]]
链接可以访问到它,从而可以知道自己是由哪个构造函数创建的。 既然我们可以通过 foo.__proto__
访问到 Foo.prototype
,同时,修改 Foo.prototype
就会影响到 foo.__proto__
那么我们直接修改 foo.__proto__
呢?
可以看到我们修改成功了,新生成的 foo2
对象也具有hacker
属性
更进一步,我们是否可以直接修改 Object.prototype
?
通过 foo1.__proto__.__proto__
访问到 Object.prototype
,再给其添加上属性,这样原来的对象a也被加上了一个 hacker
属性。
哪些情况下我们可以设置 __proto__
的值呢?找到能够控制数组(对象)的“键名”的操作即可:
比如:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
试验一下
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
这里在赋值的过程中,可能存在 o1.__proto__.b = 2
,但是并不成功
从图中可以看到,o2.__proto__
已经被解析过一次了,导致不存在 o1.__proto__.b = 2
这一步
换一种方式,变成json格式,这样就不会被提前解析了。
function merge(target, source) {
debugger;
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
从图中看到,此时正在向 Object.prototype
上添加属性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。