共计 6735 个字符,预计需要花费 17 分钟才能阅读完成。
前言
系列首发于公众号『前端进阶圈』,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。
实践 + 实际:从原型链到继承模式,把握 Object 的精华(二)
前言
-
上篇文章中介绍了对象,那本篇文章将持续介绍类相干的面向对象编程和原型。
- 咱们晓得类中有这三个要害的概念:实例化(instantiation), 继承(inheritance), 绝对多态(polymorphism),首先从实践说起。
类
类实践
- 在面向对象编程中强调的是数据和操作数据的行为在实质上是相互关联的,因而好的设计就是要把数据以及相干的行为封装起来。
多态:父类的一些通用行为能够被子类的行为重写。
- 在类中强烈建议父类和子类应用雷同的办法名来示意特定的行为,从而让子类重写父类。
- 多态并不示意父类和子类有关联,子类失去只是父类的一个正本,类的继承就是复制。
类机制
- 在许多面向类的语言中 (比方 Java) 中,” 规范库 ” 会提供 stack 类,而它是一种栈构造,反对压入 (push),弹出(pop) 等。
- 一个类其实就是一张蓝图,只是一个打算,并不是真正的能够交互的对象,咱们必须通过实例化来调用所有私有数据属性,而这个
实例化对象就是类的所有个性的一份正本
。
类的继承
- 在类的继承中,所说的父类和子类并不是实例,而是该当把父类和子类成为父类 DNA 和子类 DNA,咱们须要依据这些 DNA 来实例化一个对象,通过这个对象来以此进行沟通。
- 在类被继承时,行为也会被复制到子类中。
原型
[[Prototype]]
- JavaScript 对象中有一个非凡的
[[Prototype]]
内置属性,这其实是对其余对象的援用。在所有对象创立时[[Prototype]]
属性都会被赋予一个非空的值。 -
思考以下代码:
var myObject = {a: 2}; myObject.a; // 2
[[Prototype]]
援用有什么用呢?在之前的文章中咱们说过,当视图援用对象的属性时会触发 [[Get]] 操作,比方myObject.a
。对于默认的 [[Get]] 操作来说,第一步是查看对象自身是否有这个属性,如果有的话就应用它。但如果a
不在myObject
中,就须要应用对象的[[Prototype]]
原型链了。-
思考以下代码:
var anotherObject = {a: 2}; var myObject = Object.create(anotherObject); myObject.a; // 2
- 下面用到了
Object.create()
办法,咱们能够暂且先不思考它,后续会聊到。 - 当初
myObject
对象的[[Prototype]]
关联到了anotherObject
上。显然myObject.a
并不存在,然而属性拜访依然能胜利地(在anotherObject
) 找到了a
。 - 但如果
anotherObject
中也找不到a
并且[[Prototype]]
链不为空的话,就会持续查找上来。 - 这个查找过程会继续找到匹配的属性名或查找残缺条
[[Prototype]]
链。如果找到残缺的[[Prototype]]
链的话,[[Get]] 操作的就会返回undefined。
- 上面来看操作符对 object 有哪些作用?
- 应用
for...in
遍历对象和in
操作符时都会查找对象的整条原型链
。(无论属性是否可枚举) -
如下代码:
var anotherObject = {a:2}; // 创立一个关联到 anotherObject 的对象 var myObject = Object.create(anotherObject); for (var k in myObject) {console.log("found:" + k); } // found: a ("a" in myObject); // true
- 留神:
当你通过各种语法进行属性查找时都会查找
[[Prototype]]链,直到找到属性或找到残缺的原型链。
Object.prototype
-
但哪里是
[[Prototype]]
的止境呢?- 所有一般的
[[Prototype]]
链最终都会指向内置的Object.prototype
。
- 所有一般的
属性设置和屏蔽
- 之前说过,给一个对象设置属性不仅仅是增加一个新属性或批改已有的属性值那么简略,上面来聊一下残缺的这个过程。
-
如下代码:
myObject.foo = 'bar';
- 如果
myObject
中存在foo
属性,那以上这条赋值语句只会批改已有的属性值。 - 如果
myObject
中不存在foo
属性,[[Prototype]]
原型链就会遍历查找,相似于[[Get]]
操作,如果原型链上找不到foo
,foo
就会被增加到myObject
上。 - 如果
foo
存在于原型链的下层,以上赋值语句的行为就会有些不同,后续会聊到。 - 如果
foo
属性即存在于myObject
中,也呈现在myObject
的[[Prototype]]
原型链下层,那就会产生屏蔽
。myObject
中的foo
属性会屏蔽原型链下层的所有foo
属性,因为myObject.foo
总会抉择原型链中最底层的foo
属性。 -
如下代码:
let a = {foo: "afoo",}; let c = Object.create(a); c.foo = "cfoo"; console.log("c ------>", c.foo); // cfoo
-
咱们能够剖析下如果 foo 不间接存在于 myObject 中而是存在于原型链下层时
myObject.foo = 'bar';
会呈现三种状况:- 如果在
[[Prototype]]
原型链下层存在 foo 拜访属性,并且没有被标记为只读(writable: false)
, 那就会间接在 myObject 中增加一个 foo 属性,则它是屏蔽属性。 - 如果在
[[Prototype]]
原型链上存在 foo 属性,然而被标记为只读, 那就无奈批改已有属性或在 myObject 上创立屏蔽属性。如果在严格模式下运行,会间接抛出一个谬误。否则,这条赋值语句就会被疏忽。总之,不会产生屏蔽。 -
如果在
[[Prototype]]
原型链下层存在 foo 并且它是一个setter
,那就肯定会调用这个setter
。foo 不会被增加到(能够说屏蔽到) myObject 中,也不会从新定义 foo 这个setter
。如下代码:let a = {get foo() {return this._foo_;}, set foo(v) {this._foo_ = 'afoo';}, }; a.foo = 'afoo'; let c = Object.create(a); c.foo = "cfoo"; console.log("c ------>", c.foo); // afoo // 把赋值[[put]] 操作存储到了另一个变量 _a_ 中,名称 _a_ 只是一种常规,没有任何非凡行为,与其余一般属性一样。
- 如果在
- 很多状况下,咱们都会认为如果向
[[Prototype]]
链下层曾经存在的属性进行 [[Put]] 赋值操作,就肯定会触发屏蔽,但如你所见,三种状况中只有第一种是这样的。 - 如果你心愿在第二种和第三种状况下也屏蔽 foo, 那就不能应用
=
操作符来赋值,而是应用Object.defineProperty(...)
来向 myObject 中增加 foo。 - 留神:第二种状况下,只读属性会阻止
[[Prototype]]
原型链上层隐式屏蔽同名属性。而这个限度仅存在于=
操作符中,应用Object.defineProperty(...)
则不会受到影响。
类
- JavaScript 与其余面向类的语言不同,它并没有用类作为对象的形象模式或蓝图,而 JavaScript 只有对象。
-
在面向类的语言中,
类能够或实例化屡次
。function Foo(){//}; var a = new Foo(); Object.getPrototypeof(a) === Foo.prototype; // true
- 在以上代码中,new Foo() 这个函数调用实际上并没有间接创立关联,这个关联只是一个意外的副作用。new Foo() 只是间接实现了咱们的指标,一个关联到其余对象的新对象。
在对象中,继承意味着复制操作
,JavaScript 默认状况下不会复制对象属性,只会在两个对象之间创立一个关联。
构造函数
function Foo(){//}
var a = new Foo();
-
在以上代码为什么会让咱们认为 Foo 是一个类呢?
- 因为咱们看到了关键字
new
,在面向对象的语言中结构类实例时也会用到它。另一个起因就是,看起来咱们执行了类的构造函数办法,而 Foo() 的调用形式很像初始化类时构造函数的调用形式。
- 因为咱们看到了关键字
-
除了构造函数外,Foo.prototype 还有一个绝招,如下代码:
function Foo(){} Foo.prototype.constructor === Foo; // true var a = new Foo(); a.constructor === Foo(); // true // 实际上 .constructor 援用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo
- Foo.prototype 有一个私有且不可枚举的属性 .constructor。而在上述代码中的
constructor 属性援用的是对象关联的函数
(上述代码中是 Foo)。 - 还有一个 JavaScript 常规,
类 名首字母要大写
,所以 Foo 而非 foo,这也提醒这它是一个类
。
是构造函数还是调用
- 上一段代码很容易让人认为 Foo 是一个构造函数,因为咱们应用 new 来调用它并看到它 “ 结构 ” 了一个对象。
- 实际上,
Foo 和一般函数没有任何区别
。函数自身并不是构造函数
。然而当你在一般的函数调用前加上 new 关键字后,就会把以后函数编程一个结构函数调用
。实际上,new 会劫持所有一般函数并用结构对象的模式来调用它
。 -
如下代码:
function NothingSpecial() {console.log( "Don't mind me!"); } var a = new NothingSpecial(); // "Don't mind me!" a; // {}
- NothingSpecial 只是一个一般函数,但应用 new 调用时,它就会结构成一个对象并赋值给 a。而这个调用时一个结构函数调用,但 NothingSpecial 自身并不是一个构造函数。
在 JavaScript 中对于构造函数最精确的解释是,所有带 new 的函数调用。
所以,函数不是构造函数,然而仅当应用 new 时,函数调用就会被变成 结构函数调用。
对象关联
- 当初咱们理解了
[[Prototype]]
机制就是存在于对象中的一个外部链接,他会援用到其余对象。 -
何为原型链?
- 如果在对象上没有找到须要的属性或办法援用,引擎就会先会在以后对象中查找,如果找不到,就在
[[Prototype]]
关联的对象进行查找。如果后者也没有找到须要的援用就会持续查找它的[[Prototype]]
,直到Object.prototype
为止。以此类推,这一系列的对象链接被称为 “ 原型链 ”。
- 如果在对象上没有找到须要的属性或办法援用,引擎就会先会在以后对象中查找,如果找不到,就在
创立关联
- 那
[[Prototype]]
机制的意义是什么?为什么要创立这些关联呢? -
如下代码:
var foo = {something: function(){console.log('do something'); } }; var bar = Object.create(foo); var.something(); // do something
- 应用
Object.create() 创立了一个新对象且关联到 foo
, 这样就可防止一些不必要的麻烦(比方应用 new 的结构函数调用会生成 .prototype 和 .constructor 援用) - 留神:
Object.create(null) 会创立一个空链接的对象,因为是空的,所有无奈进行委托,并且因为这个对象没有原型链,在应用 instanceof 时也就无奈进行判断,因而他们总是会返回 false。
-
在上述的代码中咱们通过创立一个新的类来创立两个对象之间的关系,其实并不需要这么做,只须要通过委托关系来关联对象就足够了。
// Object.create()的 polyfill 代码 if(Object.create){Object.create = function(o){function F(){}; F.prototype = o; return new F();} }
- 上述代码应用一个一次性函数 F, 通过改写它的 .prototype 属性使其指向想要关联的对象,而后再应用 new F() 来结构一个新对象进行关联。
-
Object.create() 的扩大:
var anotherObject = {a:2}; var myObject = Object.create( anotherObject, { b: { enumerable: false, writable: true, configurable: false, value: 3 }, c: { enumerable: true, writable: false, configurable: false, value: 4 } }); myObject.hasOwnProperty("a"); // false myObject.hasOwnProperty("b"); // true myObject.hasOwnProperty("c"); // true myObject.a; // 2 myObject.b; // 3 myObject.c; // 4
- Object.create() 的第二个参数指定了须要增加到新对象中的属性名以及这些属性的属性描述符。
小结
- 当拜访对象中不存在的一个属性时,[[Get]] 操作就会查找对象外部
[[Prototype]]
关联的对象,这个关联关系就是一条 “ 原型链 ”(有点像嵌套的作用域),在找到属性时会对它进行遍历。 - 所有一般对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域), 如果在原型链中找不到指定的属性就会进行。
- 关联两个对象最罕用的办法就是用 new 关键字进行函数调用,在调用的第四个步骤中会创立一个关联到创立的新对象。
- 应用 new 调用函数时会把新对象的 .prototype 属性关联到其余对象,带 new 的函数调用被称为结构函数调用
- 对象之间是通过
[[Prototype]]
链关联的。 - Object.create(null) 会创立一个空链接的对象,因为是空的,所有无奈进行委托,并且因为这个对象没有原型链,在应用 instanceof 时也就无奈进行判断,因而他们总是会返回 false。
-
如果对象中的属性不间接存在于以后对象中而是存在于原型链下层时会呈现三种状况:
- 如果在
[[Prototype]]
原型链下层存在对象中的属性拜访属性,并且没有被标记为只读(writable: false)
, 那就会间接在以后对象中增加一个对象中的属性属性,则它是屏蔽属性。 - 如果在
[[Prototype]]
原型链上存在对象中的属性属性,然而被标记为只读, 那就无奈批改已有属性或在以后对象上创立屏蔽属性。如果在严格模式下运行,会间接抛出一个谬误。否则,这条赋值语句就会被疏忽。总之,不会产生屏蔽。 - 如果在
[[Prototype]]
原型链下层存在对象中的属性并且它是一个setter
,那就肯定会调用这个setter
。对象中的属性不会被增加到 (能够说屏蔽到) 以后对象中,也不会从新定义对象中的属性这个setter
。
- 如果在
- 应用
for...in
遍历对象和in
操作符时都会查找对象的整条原型链
。(无论属性是否可枚举) - 一个类其实就是一张蓝图,只是一个打算,并不是真正的能够交互的对象,咱们必须通过实例化来调用所有私有数据属性,而这个
实例化对象就是类的所有个性的一份正本
。 - 多态:父类的一些通用行为能够被子类的行为重写。
- 多态并不示意父类和子类有关联,子类失去只是父类的一个正本,类的继承就是复制。
特殊字符形容:
- 问题标注
Q:(question)
- 答案标注
R:(result)
- 注意事项规范:
A:(attention matters)
- 详情形容标注:
D:(detail info)
- 总结标注:
S:(summary)
- 剖析标注:
Ana:(analysis)
-
提醒标注:
T:(tips)
往期举荐:
- 前端面试实录 HTML 篇
- 前端面试实录 CSS 篇
- JS 如何判断一个元素是否在可视区域内?
- Vue2、3 生命周期及作用?
- 排序算法:QuickSort
- 箭头函数与一般函数的区别?
- 这是你了解的 CSS 选择器权重吗?
- JS 中 call, apply, bind 概念、用法、区别及实现?
- 罕用位运算办法?
- Vue 数据监听 Object.definedProperty()办法的实现
- 为什么 0.1+ 0.2 != 0.3,如何让其相等?
- 聊聊对 this 的了解?
-
JavaScript 为什么要进行变量晋升,它导致了什么问题?
最初:
- 欢送关注『前端进阶圈』公众号,一起摸索学习前端技术 ……
- 公众号回复 加群 或 扫码, 即可退出前端交流学习群,一起高兴摸鱼和学习 ……
- 公众号回复 加好友,即可添加为好友
正文完