「知识复盘」一文搞懂Javascript对象,原型,继承

每写一个对象直接量相当于创建了一个新的对象。即使两个对象直接量看起来一模一样,它们指向的堆内存地址也是不一样的,而对象是按引用访问的,所以这两个对象是不相等的。

var xiaoMing1 = { name: ‘小明’ };

var xiaoMing2 = { name: ‘小明’ };

xiaoMing1 === xiaoMing2; // false

new 构造函数

可以通过关键词new调用javascript对象的构造函数来获得对象实例。比如:

  1. 创建内置对象实例

var o = new Object();

  1. 创建自定义对象实例

function Person(name) {

this.name = name;

};

new Person(‘Faker’);

Object.create

Object.create用于创建一个对象,接受两个参数,使用语法如下;

Object.create(proto[, propertiesObject]);

第一个参数proto用于指定新创建对象的原型;

第二个参数propertiesObject是新创建对象的属性名及属性描述符组成的对象。

proto可以指定为null,但是意味着新对象的原型是null,它不会继承Object的方法,比如toString()等。

propertiesObject参数与Object.defineProperties方法的第二个参数格式相同。

var o = Object.create(Object.prototype, {

// foo会成为所创建对象的数据属性

foo: {

writable:true,

configurable:true,

value: “hello”

},

// bar会成为所创建对象的访问器属性

bar: {

configurable: false,

get: function() { return 10 },

set: function(value) {

console.log(“Setting o.bar to”, value);

}

}

});

属性查询和设置


属性查询

属性查询也可以称为属性访问。在javascript中,对象属性查询非常灵活,支持点号查询,也支持字符串索引查询(之所以说是“字符串索引”,是因为写法看起像数组,索引是字符串而不是数字)。

通过点号加属性名访问属性的行为很像一些静态类型语言,如java,C等。属性名是javascript标识符,必须直接写在属性访问表达式中,不能动态访问。

var o = { name: ‘小明’ };

o.name; // “小明”

而根据字符串索引查询对象属性就比较灵活了,属性名就是字符串表达式的值,而一个表达式是可以接受变量的,这意味着可以动态访问属性,这赋予了javascript程序员很大的灵活性。下面是一个很简单的示例,而这种特性在业务实践中作用很大,比如深拷贝的实现,你往往不知道你要拷贝的对象中有哪些属性。

var o = { chineseName: ‘小明’, englishName: ‘XiaoMing’ };

[‘chinese’, ‘english’].forEach(lang => {

var property = lang + ‘Name’;

console.log(o[property]); // 这里使用了字符串索引访问对象属性

})

对了,属性查询不仅可以查询自由属性,也可以查询继承属性。

var protoObj = { age: 18 };

var o = Object.create(protoObj);

o.age; // 18,这里访问的是原型属性,也就是继承得到的属性

属性设置

通过属性访问表达式,我们可以得到属性的引用,就可以据此设置属性了。这里主要注意一下只读属性和继承属性即可,细节不再展开。

原型和继承


原型

前面也提到了,原型是实现继承的基础。那么如何去理解原型呢?

首先,要明确原型概念中的三角关系,三个主角分别是构造函数,原型,实例。我这里画了一张比较简单的图来帮助理解下。

原型这东西吧,我感觉“没人能帮你理解,只有你自己去试过才是懂了”。

不过这里说说我刚学习原型时的疑惑,疑惑的是为什么构造函数有属性prototype指向原型,而实例又可以通过__proto__指向原型,究竟prototype__proto__谁是原型?其实这明显是没有理解对象是按引用访问这个特点了。原型对象永远只有一个,它存储于堆内存中,而构造函数的prototype属性只是获得了原型的引用,通过这个引用可以操作原型。

同样地,__proto__也只是原型的引用,但是要注意了,__proto__不是ECMAScript规范里的东西,所以千万不要用在生产环境中。

至于为什么不可以通过__proto__访问原型,原因也很简单。通过实例直接获得了原型的访问和修改权限,这本身是一件很危险的事情。

举个例子,这里有一个类LatinDancer,意思是拉丁舞者。经过实例化操作,得到了多个拉丁舞者。

function LatinDancer(name) {

this.name = name;

};

LatinDancer.prototype.dance = function() {

console.log(this.name + ‘跳拉丁舞…’);

}

var dancer1 = new LatinDancer(‘小明’);

var dancer2 = new LatinDancer(‘小红’);

var dancer3 = new LatinDancer(‘小王’);

dancer1.dance(); // 小明跳拉丁舞…

dancer2.dance(); // 小红跳拉丁舞…

dancer3.dance(); // 小王跳拉丁舞…

大家欢快地跳着拉丁舞,突然小王这个家伙心血来潮,说:“我要做b-boy,我要跳Breaking”。于是,他私下改了原型方法dance()

dancer3.proto.dance = function() {

console.log(this.name + ‘跳breaking…’);

}

dancer1.dance(); // 小明跳breaking…

dancer2.dance(); // 小红跳breaking…

dancer3.dance(); // 小王跳breaking…

这个时候就不对劲了,小明和小红正跳着拉丁,突然身体不受控制了,跳起了Breaking,心里暗骂:“沃尼玛,劳资不是跳拉丁的吗?”

这里只是举个例子哈,没有对任何舞种或者舞者不敬的意思,抱歉抱歉。

所以,大家应该也明白了为什么不能使用__proto__了吧。

原型链

在javascript中,任何对象都有原型,除了Object.prototype,它没有原型,或者说它的原型是null

那么什么是原型链呢?javascript程序在查找一个对象的属性或方法时,会首先在对象本身上进行查找,如果找不到则会去对象的原型上进行查找。按照这样一个递归关系,如果原型上找不到,就会到原型的原型上找,这样一直查找下去,就会形成一个链,它的终点是null

还要注意的一点是,构造函数也是一个对象,也存在原型,它的原型可以通过Function.prototype获得,而Function.prototype的原型则可以通过Object.prototype获得。

继承

说到继承,可能大家脑子里已经冒出来“原型链继承”,“借用构造函数继承”,“寄生式继承”,“原型式继承”,“寄生组合继承”这些概念了吧。说实话,一开始我也是这么记忆,但是发现好像不是那么容易理解啊。最后,我发现,只要从原型三角关系入手,就能理清实现继承的思路。

我们知道,对象实例能访问的属性和方法一共有三个来源,分别是:调用构造函数时挂载到实例上的属性,原型属性,对象实例化后自身新增的属性。

很明显,第三个来源不是用来做继承的,那么前两个来源用来做继承分别有什么优缺点呢?很明显,如果只基于其中一种来源做继承,都不可能全面地继承来自父类的属性或方法。

首先明确下继承中三个主体:父类子类子类实例。那么怎么才能让子类实例和父类搭上关系呢?

原型链继承

所谓继承,简单说就是能通过子类实例访问父类的属性和方法。而利用原型链可以达成这样的目的,所以只要父类原型、子类原型、子类实例形成原型链关系即可。

代码示例:

function Father() {

this.nationality = ‘Han’;

};

Father.prototype.propA = ‘我是父类原型上的属性’;

function Child() {};

Child.prototype = new Father();

Child.prototype.constructor = Child; // 修正原型上的constructor属性

Child.prototype.propB = ‘我是子类原型上的属性’;

var child = new Child();

console.log(child.propA, child.propB, child.nationality); // 都可以访问到

child instanceof Father; // true

可以看到,在上述代码中,我们做了这样一个特殊处理Child.prototype.constructor = Child;。一方面是为了保证constructor的指向正确,毕竟实例由子类实例化得来,如果constructor指向父类构造函数也不太合适吧。另一方面是为了防止某些方法显示调用constructor时带来的麻烦。具体解释见Why is it necessary to set the prototype constructor?[1]

关键点:让子类原型成为父类的实例,子类实例也是父类的实例。

缺点:实例化时无法向父类构造函数传参。

借用构造函数

在调用子类构造函数时,通过call调用父类构造函数,同时指定this值。

function Father() {

this.nationality = ‘Han’;

};

Father.prototype.propA = ‘我是父类原型上的属性’;

function Child() {

Father.call(this);

};

Child.prototype.propB = ‘我是子类原型上的属性’;

var child = new Child();

console.log(child.propA, child.propB, child.nationality);

这里的child.propAundefined,因为子类实例不是父类的实例,无法继承父类原型属性。

child instanceof Father; // false

关键点:构造函数的复用。

缺点:子类实例不是父类的实例,无法继承父类原型属性。

组合继承

所谓组合继承,就是综合上述两种方法。实现代码如下:

function Father() {

this.nationality = ‘Han’;

};

Father.prototype.propA = ‘我是父类原型上的属性’;

function Child() {

Father.call(this);

};

Child.prototype = new Father();

Child.prototype.constructor = Child; // 修正原型上的constructor属性

Child.prototype.propB = ‘我是子类原型上的属性’;

var child = new Child();

console.log(child.propA, child.propB, child.nationality); // 都能访问到

一眼看上去没什么问题,但是Father()构造函数其实是被调用了两次的。第一次发生在Child.prototype = new Father();,此时子类原型成为了父类实例,执行父类构造函数Father()时,获得了实例属性nationality;第二次发生在var child = new Child();,此时执行子类构造函数Child(),而Child()中通过call()调用了父类构造函数,所以子类实例也获得了实例属性nationality。这样理解起来可能有点晦涩难懂,我们可以看看子类实例的对象结构:

可以看到,子类实例和子类原型上都挂载了执行父类构造函数时获得的属性nationality。然而我们做继承的目的是很单纯的,即“让子类继承父类属性和方法”,但并不应该给子类原型挂载不必要的属性而导致污染子类原型。

有人会说“这么一点副作用怕什么”。当然,对于这么简单的父类而言,这种副作用微乎其微。假设父类有几百个属性或方法呢,这种白白耗费性能和内存的行为是有必要的吗?答案显而易见。

关键点:实例属性和原型属性都得以继承。

缺点:父类构造函数被执行了两次,污染了子类原型。

原型式继承

原型式继承是相对于原型链继承而言的,与原型链继承的不同点在于,子类原型在创建时,不会执行父类构造函数,是一个纯粹的空对象。

function Father() {

this.nationality = ‘Han’;

};

Father.prototype.propA = ‘我是父类原型上的属性’;

function Child() {};

Child.prototype = Object.create(Father.prototype);

Child.prototype.constructor = Child; // 修正原型上的constructor属性

Child.prototype.propB = ‘我是子类原型上的属性’;

var child = new Child();

console.log(child.propA, child.propB, child.nationality); // 都可以访问到

child instanceof Father; // true

ES5之前,可以这样模拟Object.create

function create(proto) {

function F() {}

F.prototype = proto;

return new F();

}

关键点:利用一个空对象过渡,解除子类原型和父类构造函数的强关联关系。这也意味着继承可以是纯对象之间的继承,无需构造函数介入。

缺点:实例化时无法向父类构造函数传参,这一点和原型链继承并无差异。

寄生式继承

寄生式继承有借鉴工厂函数的设计模式,将继承的过程封装到一个函数中并返回对象,并且可以在函数中扩展对象方法或属性。

var obj = {

nationality: ‘Han’

};

function inherit(proto) {

var o = Object.create(proto);

o.extendFunc = function(a, b) {

return a + b;

}

return o;

}

var inheritObj = inherit(obj);

这里inheritObj不仅继承了obj,而且也扩展了extendFunc方法。

关键点:工厂函数,封装过程函数化。

缺点:如果在工厂函数中扩展对象属性或方法,无法得到复用。

寄生组合继承

用以解决组合继承过程中存在的“父类构造函数多次被调用”问题。

function inherit(childType, fatherType) {

childType.prototype = Object.create(fatherType.prototype);

childType.prototype.constructor = childType;

}

function Father() {

this.nationality = ‘Han’;

}

Father.prototype.propA = ‘我是父类原型上的属性’;

function Child() {

Father.call(this)

}

inherit(Child, Father); // 继承

Child.prototype.propB = ‘我是子类原型上的属性’;

var child = new Child();

console.log(child);

关键点:解决父类构造函数多次执行的问题,同时让子类原型变得更加纯粹。

静态方法

何谓“静态方法”?静态方法为类所有,不归属于任何一个实例,需要通过类名直接调用。

function Child() {}

Child.staticMethod = function() { console.log(“我是一个静态方法”) }

var child = new Child();

Child.staticMethod(); // “我是一个静态方法”

child.staticMethod(); // Uncaught TypeError: child.staticMethod is not a function

Object类有很多的静态方法,我学习的时候习惯把它们分为这么几类(当然,这里没有全部列举开来,只挑了常见的方法)。

创建和复制对象
  • Object.create():基于原型和属性描述符集合创建一个新对象。

  • Object.assign():合并多个对象,会影响源对象。所以在合并对象时,为了避免这个问题,一般会这样做:

var mergedObj = Object.assign({}, a, b);

属性相关
  • Object.defineProperty:通过属性描述符来定义或修改对象属性,主要涉及value, configurable, writable, enumerable四个特性。

  • Object.defineProperties:是defineProperty的升级版本,一次性定义或修改多个属性。

  • Object.getOwnPropertyDescriptor:获取属性描述符,是一个对象,包含value, configurable, writable, enumerable四个特性。

  • Object.getOwnPropertyNames:返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

  • Object.keys:会返回一个由一个给定对象的自身可枚举属性组成的数组,与getOwnPropertyNames最大的不同点在于:keys只返回enumerabletrue的属性,并且会返回原型对象上的属性。

原型相关
  • Object.getPrototypeOf:返回指定对象的原型。

function Child() {}

var child = new Child();

Object.getPrototypeOf(child) === Child.prototype; // true

  • Object.setPrototypeOf:设置指定对象的原型。这是一个比较危险的动作,同时也是一个性能不佳的方法,不推荐使用。
行为控制

以下列举的这三个方式是一个递进的关系,我们按序来看:

  • Object.preventExtensions:让一个对象变的不可扩展,也就是永远不能再添加新的属性。

  • Object.seal:封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。也就是说Object.sealObject.preventExtensions的基础上,给对象属性都设置了configurablefalse

这里有一个坑是:对于configurablefalse的属性,虽然不能重新设置它的configurableenumerable特性,但是可以把它的writable特性从true改为false(反之不行)。

  • Object.freeze:冻结一个对象,不能新增,修改,删除属性,也不能修改属性的原型。这里还有一个深冻结deepFreeze的概念,有点类似深拷贝的意思,递归冻结。
检测能力

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

这里有一个坑是:对于configurablefalse的属性,虽然不能重新设置它的configurableenumerable特性,但是可以把它的writable特性从true改为false(反之不行)。

  • Object.freeze:冻结一个对象,不能新增,修改,删除属性,也不能修改属性的原型。这里还有一个深冻结deepFreeze的概念,有点类似深拷贝的意思,递归冻结。
检测能力

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-ynUhO7Qd-1715743900018)]

[外链图片转存中…(img-M8vAIKGf-1715743900019)]

[外链图片转存中…(img-hWDq8Z1o-1715743900019)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值