网站域名跟谁买,c 网站开发工程师招聘,终身免费vps,蔺市网站建设#x1f601; 作者简介#xff1a;一名大四的学生#xff0c;致力学习前端开发技术 ⭐️个人主页#xff1a;夜宵饽饽的主页 ❔ 系列专栏#xff1a;JavaScript进阶指南 #x1f450;学习格言#xff1a;成功不是终点#xff0c;失败也并非末日#xff0c;最重要的是继… 作者简介一名大四的学生致力学习前端开发技术 ⭐️个人主页夜宵饽饽的主页 ❔ 系列专栏JavaScript进阶指南 学习格言成功不是终点失败也并非末日最重要的是继续前进的勇气 前言 有关对象中的原型和原型链这里面有很多的知识体系对于构造函数和所谓的原型继承到底是什么,对于constructor的理解比较模糊还有在javaScript中所谓的“类”和对象是什么关系这些问题在本篇都可以了解到请大家认真理解本篇博客会受益匪浅的希望可以帮助到大家欢迎大家的补充和纠正 文章目录 原型1 [[ Prototype ]]2 属性设置和屏蔽3 类3.1 “类”函数3.2 构造函数与类的误解3.3 构造函数是什么3.4 构造函数的属性是什么3.5 实例的constructor属性5.3 原型继承 4 对象关联4.1 创建关联4.2 关联关系的意义 原型
1 [[ Prototype ]]
JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值 思考以下代码
//第一种情况
var myObject{a:2
}//当对象上有这个属性时
myObject.a //2//第二种情况
var anotherObject{a:2
}
// 创建一个关联到 anotherObject 的对象
var myObjectObject.create(anotherObject)
//当对象上没有对象的原型上有时
myObject.a //2当我们引用对象属性时会触发Get操作。比如上述代码中myObject.a。
第一步会检查myObject对象本身是否有这个属性如果有就用它当myObject对象本身没有时会进行第二步检查myObject对象的[[Prototype链]]代码中是anotherObject对象)如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话就会继续查找下去这个过程会持续找到匹配的属性名或者完整条[[Prototype]]链如果后者的话Get操作会返回undefined
2 属性设置和屏蔽
当我们给对象设置属性并不是仅仅是添加一个新属性或者修改已有的属性值
接下来我们完整的讲解一下这个过程
myObject.foobar如果 myObject 对象中包含名为 foo 的普通数据访问属性这条赋值语句只会修改已有的属性值如果 foo 不是直接存在于 myObject 中[[Prototype]] 链就会被遍历类似 Get 操作。如果原型链上找不到 foofoo 就会被直接添加到 myObject 上。 这种情况下如果foo存在原型链上层赋值语句会行为会有所不同下面我们会仔细介绍如果属性名 foo 既出现在 myObject 中也出现在 myObject 的 [[Prototype]] 链上层那么就会发生屏蔽。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性因为myObject.foo 总是会选择原型链中最底层的 foo 属性 屏蔽 比我们想象中的更加复杂下面我们分析一下如果foo不直接存在于myObject中而是存在于原型链上层myObject.foobar’的三种情况
如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且没有被标记为只读writable:false那就会直接在 myObject 中添加一个名为 foo 的新属性它是屏蔽属性如果在 [[Prototype]] 链上层存在 foo但是它被标记为只读writable:false那么无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下代码会抛出一个错误。否则这条赋值语句会被忽略。总之不会发生屏蔽如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter那就一定会调用这个 setter。foo 不会被添加到或者说屏蔽于myObject也不会重新定义 foo 这个 setter。 小贴士如果希望在第二种和第三种情况下也屏蔽 foo那就不能使用 操作符来赋值而是使用 Object.defineProperty(…)来向 myObject 添加 foo。 ❗️ 以下代码会产生隐式屏蔽需要注意
var anotherObject {
a:2
};
var myObject Object.create( anotherObject );
anotherObject.a; // 2myObject.a; // 2anotherObject.hasOwnProperty( a ); // true
myObject.hasOwnProperty( a ); // falsemyObject.a; // 隐式屏蔽anotherObject.a; // 2
myObject.a; // 3myObject.hasOwnProperty( a ); // true尽管 myObject.a 看起来应该通过委托查找并增加 anotherObject.a 属性但是别忘了 操作相当于 myObject.a myObject.a 1。因此 操作首先会通过 [[Prototype]]查找属性 a 并从 anotherObject.a 获取当前属性值 2然后给这个值加 1接着将值 3 赋给 myObject 中新建的屏蔽属性 a
3 类
现在你可能会很好奇为什么一个对象需要关联到另一个对象这样做有什么好处这个问题非常好但是在回答之前我们首先要理解 [[Prototype]]“不是”什么。
JavaScript 和面向类的语言不同它并没有类来作为对象的抽象模式或者说蓝图。JavaScript中只有对象
实际上JavaScript才是真正应该被称为“面向对象”语言因为它是少有的可以不通类直接创建对象的语言。
在 JavaScript 中类无法描述对象的行为因为根本就不存在类对象直接定义自己的行为。再说一遍JavaScript 中只有对象。
3.1 “类”函数
在JavaScript中是没有类的只有对象所以可能会产生一种奇怪的行为那就是 模仿类我们会仔细分析这种方法
这种奇怪的类似类的行为利用函数的一种特殊特性所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性它指向另一个对象
function Foo(){}Foo.prototype //{}这个对象通常被称为Foo原型因为我们通过名为 Foo.prototype 的属性引用来访问它。 那么这个对象到底是什么 这个对象是在调用new Foo()时创建的最后会被关联到这个“Foo.prototype” 对象上
我们来验证一下
function foo(){//...
}
var anew Foo()Object.getProtypeOf(a) Foo.prototype //true调用new Foo()时会创建a关于this那章)其中的第二步就是给a一个内部的[[Protype]] 链接关联到Foo.prototype指向那个对象 接下来我们来思考一下一些问题
面向类的语言有什么独特JavaScript模仿类的行为是什么 理解如下
在面向类的语言中类可以被复制或者说实例化多次就像用模具制作东西一样。之所以会这样是因为实例化或者继承一个类就意味着“把类的行为复制到物理对象中”对于每一个新实例来说都会重复这个过程。
但是在 JavaScript 中并没有类似的复制机制。你不能创建一个类的多个实例只能创建多个对象它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制因此这些对象之间并不会完全失去联系它们是互相关联的。
new Foo() 会生成一个新对象我们称之为 a这个新对象的内部链接 [[Prototype]] 关联的是 Foo.prototype 对象。
最后我们得到了两个对象它们之间互相关联就是这样。我们并没有初始化一个类实际上我们并没有从“类”中复制任何行为到一个对象中只是让两个对象互相关联。 3.2 构造函数与类的误解
function Foo(){}
var anew Foo()到底是什么让我们认为Foo是一个类呢或者说类与构造函数的误解是什么
我觉得有两个地方会让我们产生误解 调用方式我们看到了关键字 new在面向类的语言中构造类实例时也会用到它。另一个原因是看起来我们执行了类的构造函数方法Foo() 的调用方式很像初始化类时类构造函数的调用方式。 对象的constructor属性的指向 function Foo(){}
Foo.prototype.constructor Foo //truevar anew Foo()
a.constructor Foo //trueFoo.prototype 默认在代码中第一行声明时有一个公有并且不可枚举的属性 .constructor这个属性引用的是对象关联的函数本例中是 Foo。此外我们可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 .constructor 属性指向 “创建这个对象的函数”。 ❗️ 但是实际上a本身并没有.constructor属性虽然 a.constructor 确实指向 Foo 函数但是这个属性并不是表示 a 由 Foo“构造” 稍后会解释的
3.3 构造函数是什么
上一节中的代码我们使用new来调用它并且看到它构造了一个对象
实际上Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数然而当你在普通的函数调用前面加上 new 关键字之后就会把这个函数调用变成一个“构造函数调用”。实际上new 会劫持所有普通函数并用构造对象的形式来调用它。
例子如下
function NothingSpecial() {console.log( Dont mind me! );
}
var a new NothingSpecial();
// Dont mind me! a; // {}NothingSpecial 只是一个普通的函数但是使用 new 调用时它就会构造一个对象并赋值给 a这看起来像是 new 的一个副作用无论如何都会构造一个对象。这个调用是一个构造函数调用但是 NothingSpecial 本身并不是一个构造函数。
换句话说在 JavaScript 中对于“构造函数”最准确的解释是所有带 new 的函数调用。
函数不是构造函数但是当且仅当使用new时函数调用会变成“构造函数调用”
3.4 构造函数的属性是什么
function Foo(name){this.namename
}Foo.prototype.myNamefunction(){return this.name
}var anew Foo(a)
var bnew Foo(b)a.myName() //a
b.myName() //b这段代码中有两个有意思值得思考的点
this.namename给每个对象这是在关于this的博客说过是关于this的指向绑定都添加了.name属性有点像实例封装数据值Foo.prototype.myName … 可能个更有趣的技巧它会给 Foo.prototype 对象添加一个属性函数。现在a.myName() 可以正常工作但是你可能会觉得很惊讶这是什么原理呢
在这段代码中看起来似乎创建 a 和 b 时会把 Foo.prototype 对象复制到这两个对象中然而事实并不是这样。
在本章开头介绍默认 [[Get]] 算法时我们介绍过 [[Prototype]] 链以及当属性不直接存在于对象中时如何通过它来进行查找。
因此在创建的过程中a 和 b 的内部 [[Prototype]] 都会关联到 Foo.prototype 上。当 a和 b 中无法找到 myName 时它会通过委托参见下一章在 Foo.prototype 上找到
3.5 实例的constructor属性
回顾5.2.2中的一个问题 ❗️ 但是实际上a本身并没有.constructor属性虽然 a.constructor 确实指向 Foo 函数但是这个属性并不是表示 a 由 Foo“构造” 稍后会解释的 这个问题中a.constructor Foo为真意味着a确实有指向Foo的.constructor属性但是事实不是如此 实际上.constructor引用同样被委托给了Foo.prototype而Foo.prototype.constructor默认指向Foo。
把 .constructor 属性指向 Foo 看作是 a 对象由 Foo“构造”非常容易理解但这只不过是一种虚假的安全感。a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo这和“构造”毫无关系。相反对于 .constructor 的错误理解很容易对你自己产生误导。
举例来说Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的 .prototype 对象引用那么新对象并不会自动获得 .constructor 属性。 思考一下的代码
function Foo(){ /* .. */ }Foo.prototype{ /* .. */ }var a1new Foo()a1.constructor Foo //false
a1.constructor Object //trueObject(…) 并没有“构造”a1对吧看起来应该是 Foo()“构造”了它。大部分开发者都认为是 Foo() 执行了构造工作但是问题在于如果你认为“constructor”表示“由……构造”的话a1.constructor 应该是 Foo但是它并不是 Foo
到底怎么回事 a1 并没有 .constructor 属性所以它会委托 [[Prototype]] 链上的 Foo.prototype。但是这个对象也没有 .constructor 属性不过默认的 Foo.prototype 对象有这个属性所以它会继续委托这次会委托给委托链顶端的 Object.prototype。这个对象有 .constructor 属性指向内置的 Object(…) 函数。
5.3 原型继承
实际上我们已经了解了通常被称作原型继承的机制a可以继承Foo.prototype并访问Foo.prototype的myName()函数但是我们之前只把继承看作是类是实例的关系并没有把它看作是类是和类的关系
上面的那张关系图里它不仅展示出对象实例a1 到 Foo.prototype 的委托关系还展示出Bar.prototype 到 Foo.prototype 的委托关系而后者和类继承很相似只有箭头的方向不同。图中由下到上的箭头表明这是委托关联不是复制操作。
下面这段代码使用的就是典型的“原型风格”
function Foo(name) {this.name name;
}Foo.prototype.myName function() {return this.name;
};function Bar(name,label) {Foo.call( this, name );this.label label;
}// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype Object.create( Foo.prototype );// 注意现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel function() {return this.label;
};var a new Bar( a, obj a );a.myName(); // a
a.myLabel(); // obj a这段代码的核心部分就是语句 Bar.prototype Object.create( Foo.prototype )。调用Object.create(…) 会凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你指定的对象本例中是 Foo.prototype。
换句话说这条语句的意思是“创建一个新的 Bar.prototype 对象并把它关联到 Foo.prototype”。
声明 function Bar() { … } 时和其他函数一样Bar 会有一个 .prototype 关联到默认的对象但是这个对象并不是我们想要的 Foo.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上直接把原始的关联对象抛弃掉。 如果有不明白为什么this指向a的话可以查看上一篇博客 学习JavaScript的this的使用和原理这一篇就够了超详细 ❗️ 注意下面这两种方式是替换原型常见的错误
// 和你想要的机制不一样
Bar.prototype Foo.prototype;// 基本上满足你的需求但是可能会产生一些副作用 :
Bar.prototype new Foo();Bar.prototype Foo.prototype 并不会创建一个关联到 Bar.prototype 的新对象它只是让 Bar.prototype 直接引用 Foo.prototype 对象。因此当你执行类似Bar.prototype.myLabel … 的赋值语句时会直接修改 Foo.prototype 对象本身。显然这不是你想要的结果否则你根本不需要 Bar 对象直接使用 Foo 就可以了这样代码也会更简单一些。
Bar.prototype new Foo() 的确会创建一个关联到 Bar.prototype 的新对象。但是它使用了 Foo(…) 的“构造函数调用”如果函数 Foo 有一些副作用比如写日志、修改状态、注册到其他对象、给 this 添加数据属性等等的话就会影响到 Bar() 的“后代”后果不堪设想。
因此要创建一个合适的关联对象我们必须使用 Object.create(…) 而不是使用具有副作用的 Foo(…)。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉不能直接修改已有的默认对象。
4 对象关联
现在我们知道了[[Prototype]] 机制就是存在于对象中的一个内部链接它会引用其他对象。
通常来说这个链接的作用是如果在对象上没有找到需要的属性或者方法引用引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理如果在后者中也没有找到需要的引用就会继续查找它的 [[Prototype]]以此类推。这一系列对象的链接被称为“原型链”。
4.1 创建关联
我们可以使用Object.create()来创建对象之间的关联非常简单和方便
var foo {
something: function() {
console.log( Tell me something good... );
}
};
var bar Object.create( foo );
bar.something(); // Tell me something goodObject.create(…) 会创建一个新对象bar并把它关联到我们指定的对象foo这样我们就可以充分发挥 [[Prototype]] 机制的威力委托并且避免不必要的麻烦比如使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用。 Object.create(null) 会 创 建 一 个 拥 有 空 或 者 说 null[[Prototype]]链接的对象这个对象无法进行委托。这些特殊的空 [[Prototype]] 对象通常被称作“字典”它们完全不会受到原型链的干扰因此非常适合用来存储数据。 我们并不需要类来创建两个对象之间的关系只需要通过委托来关联对象就足够了。而Object.create(…) 不包含任何“类的诡计”所以它可以完美地创建我们想要的关联关系。
Object.create()的替代代码
Object.create(…) 是在 ES5 中新增的函数所以在 ES5 之前的环境中比如旧 IE如果要支持这个功能的话就需要使用一段简单的代码片段它部分实例了Object.create(…)的功能
if (!Object.create) {Object.create function(o) {function F(){}F.prototype o;return new F();};
}这段 polyfill 代码使用了一个一次性函数 F我们通过改写它的 .prototype 属性使其指向想要关联的对象然后再使用 new F() 来构造一个新对象进行关联
4.2 关联关系的意义
看起来对象之间的关联关系是处理“缺失”属性或者方法时的一种备用选项。这个说法有点道理但是我认为这并不是 [[Prototype]] 的本质 思考以下的代码
var anotherObject {cool: function() {console.log( cool! );}
};var myObject Object.create( anotherObject );myObject.cool(); // cool!由于存在 [[Prototype]] 机制这段代码可以正常工作。但是如果你这样写只是为了让myObject 在无法处理属性或者方法时可以使用备用的 anotherObject那么你的软件就会变得有点“神奇”而且很难理解和维护。
这并不是说任何情况下都不应该选择备用这种设计模式但是这在 JavaScript 中并不是很常见。所以如果你使用的是这种模式那或许应当退后一步并重新思考一下这种模式是否合适。 在 ES6 中有一个被称为“代理”Proxy的高端功能它实现的就是“方法无法找到”时的行为,如果有想了解的小伙伴可以查看我的一篇博客 JavaScript的Proxy的使用和详情还有代理的概念问题 当你给开发者设计软件时假设要调用 myObject.cool()如果 myObject 中不存在 cool()时这条语句也可以正常工作的话那你的 API 设计就会变得很“神奇”对于未来维护你软件的开发者来说这可能不太好理解。
⭐️ 但是你可以让你的 API 设计不那么“神奇”同时仍然能发挥 [[Prototype]] 关联的威力
var anotherObject {cool: function() {console.log( cool! );}
};
var myObject Object.create( anotherObject );myObject.doCool function() {this.cool(); // 内部委托
};myObject.doCool(); // cool!这里我们调用的 myObject.doCool() 是实际存在于 myObject 中的这可以让我们的 API 设计更加清晰不那么“神奇”。从内部来说我们的实现遵循的是委托设计模式参见下一章通过 [[Prototype]] 委托到 anotherObject.cool()。
换句话说内部委托比起直接委托可以让 API 接口设计更加清晰