深度剖析前端JavaScript中的原型(JS的对象原型)
通过原型机制,JavaScript 中的对象从其他对象继承功能特性;这种继承机制与经典的面向对象编程语言的继承机制不同。本文将探讨这些差别,解释原型链如何工作,并了解如何通过 prototype
属性向已有的构造器添加方法
基于原型的语言?
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非对象实例本身。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
注意: 理解对象的原型(可以通过Object.getPrototypeOf(obj)
或者已被弃用的__proto__
属性获得)与构造函数的prototype
属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())
和Foobar.prototype
指向着同一个对象。
以上描述很抽象;我们先看一个例子。
在javascript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype)
,正如下面所展示的。请注意,下面的代码是独立的一段(在网页中没有其他代码的情况下,这段代码是安全的)。为了最好的学习体验,你最好打开一个控制台 (在Chrome和Firefox中,可以按Ctrl+Shift+I来打开)切换到"控制台" 选项卡, 复制粘贴下面的JavaScript代码,然后按回车来运行
function doSomething(){}
console.log( doSomething.prototype );
// It does not matter how you declare the function, a
// function in javascript will always have a default
// prototype property.
var doSomething = function(){};
console.log( doSomething.prototype );
正如上面所看到的, doSomething
函数有一个默认的原型属性,它在控制台上面呈现了出来. 运行这段代码之后,控制台上面应该出现了像这样的一个对象.
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
现在,我们可以添加一些属性到 doSomething 的原型上面,如下所示.
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );
结果:
{
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething
的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new
前缀. 通过这种方法,在调用函数前加一个 new
,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性:
function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );
结果:
{
prop: "some value",
__proto__: {
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}
就像上面看到的doSomeInstancing
的 __proto__
属性就是doSomething.prototype
. 但是这又有什么用呢?
好吧,当你访问 doSomeInstancing
的一个属性, 浏览器首先查找 doSomeInstancing
是否有这个属性. 如果 doSomeInstancing
没有这个属性, 然后浏览器就会在 doSomeInstancing
的 __proto__
中查找这个属性(也就是 doSomething.prototype). 如果 doSomeInstancing 的 __proto__
有这个属性, 那么 doSomeInstancing 的 __proto__
上的这个属性就会被使用. 否则, 如果 doSomeInstancing 的 __proto__
没有这个属性, 浏览器就会去查找 doSomeInstancing 的 __proto__
的 __proto__
,看它是否有这个属性.
默认情况下, 所有函数的原型属性的 __proto__
就是 window.Object.prototype
. 所以 doSomeInstancing 的 __proto__
的 __proto__
(也就是 doSomething.prototype 的 __proto__
(也就是 Object.prototype
)) 会被查找是否有这个属性. 如果没有在它里面找到这个属性, 然后就会在 doSomeInstancing 的 __proto__
的 __proto__
的 __proto__
里面查找. 然而这有一个问题: doSomeInstancing 的 __proto__
的 __proto__
的 __proto__
不存在. 最后, 原型链上面的所有的 __proto__
都被找完了, 浏览器所有已经声明了的 __proto__
上都不存在这个属性,然后就得出结论,这个属性是 undefined
.
function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop: " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo: " + doSomeInstancing.foo);
console.log("doSomething.prop: " + doSomething.prop);
console.log("doSomething.foo: " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);
结果:
doSomeInstancing.prop: some value
doSomeInstancing.foo: bar
doSomething.prop: undefined
doSomething.foo: undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo: bar
理解原型对象
让我们回到 Person()
构造器的例子。请把下面代码例子依次写入浏览器控制台。。
本例中我们首先将定义一个构造器函数:
function Person(first, last, age, gender, interests) { // 属性与方法定义 };
然后在控制台创建一个对象实例:
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
在 JavaScript 控制台输入 "person1.
",你会看到,浏览器将根据这个对象的可用的成员名称进行自动补全:
在这个列表中,你可以看到定义在 person1
的原型对象、即 Person()
构造器中的成员—— name
、age
、gender
、interests
、bio
、greeting
。同时也有一些其他成员—— watch
、valueOf
等等——这些成员定义在 Person()
构造器的原型对象、即 Object
之上。下图展示了原型链的运作机制。
那么,调用 person1
的“实际定义在 Object
上”的方法时,会发生什么?比如:
person1.valueOf()
这个方法仅仅返回了被调用对象的值。在这个例子中发生了如下过程:
- 浏览器首先检查,
person1
对象是否具有可用的valueOf()
方法。 - 如果没有,则浏览器检查
person1
对象的原型对象(即Person
构造函数的prototype属性所指向的对象)是否具有可用的valueof()
方法。 - 如果也没有,则浏览器检查
Person()
构造函数的prototype属性所指向的对象的原型对象(即Object
构造函数的prototype属性所指向的对象)是否具有可用的valueOf()
方法。这里有这个方法,于是该方法被调用。
注意:必须重申,原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式。
注意:没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中,在 JavaScript 语言标准中用 [[prototype]]
表示(参见 ECMAScript)。然而,大多数现代浏览器还是提供了一个名为 __proto__
(前后各有2个下划线)的属性,其包含了对象的原型。你可以尝试输入 person1.__proto__
和 person1.__proto__.__proto__
,看看代码中的原型链是什么样的!
prototype 属性:继承成员被定义的地方
那么,那些继承的属性和方法在哪儿定义呢?如果你查看 Object
参考页,会发现左侧列出许多属性和方法——大大超过我们在 person1
对象中看到的继承成员的数量。某些属性或方法被继承了,而另一些没有——为什么呢?
原因在于,继承的属性和方法是定义在 prototype
属性之上的(你可以称之为子命名空间 (sub namespace) )——那些以 Object.prototype.
开头的属性,而非仅仅以 Object.
开头的属性。prototype
属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。
于是 Object.prototype.watch()、
Object.prototype.valueOf()
等等成员,适用于任何继承自 Object()
的对象类型,包括使用构造器创建的新的对象实例。
Object.is()
、Object.keys()
,以及其他不在 prototype
对象内的成员,不会被“对象实例”或“继承自 Object()
的对象类型”所继承。这些方法/属性仅能被 Object()
构造器自身使用。
注意:这看起来很奇怪——构造器本身就是函数,你怎么可能在构造器这个函数中定义一个方法呢?其实函数也是一个对象类型。
- 你可以检查已有的
prototype
属性。回到先前的例子,在 JavaScript 控制台输入:Person.prototype
- 输出并不多,毕竟我们没有为自定义构造器的原型定义任何成员。缺省状态下,构造器的
prototype
属性初始为空白。现在尝试:Object.prototype
- 你可以检查已有的
你会看到 Object
的 prototype
属性上定义了大量的方法;如前所示,继承自 Object
的对象都可以使用这些方法。
JavaScript 中到处都是通过原型链继承的例子。比如,你可以尝试从 String
、Date
、Number
和 Array
全局对象的原型中寻找方法和属性。它们都在原型上定义了一些方法,因此当你创建一个字符串时:
var myString = 'This is my string.';
myString
立即具有了一些有用的方法,如 split()
、indexOf()
、replace()
等。
重要:prototype
属性大概是 JavaScript 中最容易混淆的名称之一。你可能会认为,this
关键字指向当前对象的原型对象,其实不是(还记得么?原型对象是一个内部对象,应当使用 __proto__
访问)。prototype
属性包含(指向)一个对象,你在这个对象中定义需要被继承的成员。
create()
Object.create()
方法可以创建新的对象实例。
- 例如,在上个例子的 JavaScript 控制台中输入:
var person2 = Object.create(person1);
create()
实际做的是从指定原型对象创建一个新的对象。这里以person1
为原型对象创建了person2
对象。在控制台输入:person2.__proto__
- 例如,在上个例子的 JavaScript 控制台中输入:
结果返回对象person1
。
constructor 属性
每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。
- 例如,继续在控制台中尝试下面的指令:
person1.constructor
person2.constructor都将返回
Person()
构造器,因为该构造器包含这些实例的原始定义。一个小技巧是,你可以在
constructor
属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加new
关键字,便能将此函数作为构造器使用。- 在控制台中输入:
var person3 = new person1.constructor('Karen',
'Stephenson', 26,
'female',
['playing drums', 'mountain climbing']); - 现在尝试访问新建对象的属性,例如:
person3.name.first
person3.age
person3.bio()
- 在控制台中输入:
- 例如,继续在控制台中尝试下面的指令:
正常工作。通常你不会去用这种方法创建新的实例;但如果你刚好因为某些原因没有原始构造器的引用,那么这种方法就很有用了。
此外,constructor
属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:
instanceName.constructor.name
具体地,像这样:
person1.constructor.name
修改原型
从我们从下面这个例子来看一下如何修改构造器的 prototype
属性。
向构造器的 prototype
添加了一个新的方法:
function Person(first, last, age, gender, interests) { // 属性与方法定义 }; var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']); Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
}
但是 farewell()
方法仍然可用于 person1
对象实例——旧有对象实例的可用功能被自动更新了。这证明了先前描述的原型链模型。这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。
你很少看到属性定义在 prototype 属性中,因为如此定义不够灵活。比如,你可以添加一个属性:
Person.prototype.fullName = 'Bob Smith';
但这不够灵活,因为人们可能不叫这个名字。用 name.first
和 name.last
组成 fullName
会好很多:
Person.prototype.fullName = this.name.first + ' ' + this.name.last;
然而,这么做是无效的,因为本例中 this
引用全局范围,而非函数范围。访问这个属性只会得到 undefined undefined
。但这个语句若放在 先前定义在 prototype
上的方法中则有效,因为此时语句位于函数范围内,从而能够成功地转换为对象实例范围。你可能会在 prototype
上定义常属性 (constant property) (指那些你永远无需改变的属性),但一般来说,在构造器内定义属性更好。
注:关于 this
关键字指代(引用)什么范围/哪个对象,这个问题超出了本文讨论范围。事实上,这个问题有点复杂,如果现在你没能理解,也不用担心。
事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype
属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
// 构造器及其属性定义 function Test(a,b,c,d) {
// 属性定义
}; // 定义第一个方法 Test.prototype.x = function () { ... } // 定义第二个方法 Test.prototype.y = function () { ... } // 等等……
本文介绍了 JavaScript 对象原型,包括原型链如何允许对象之间继承特性、prototype
属性、如何通过它来向构造器添加方法。
如果大家有任何疑问即可留言反馈,会在第一时间回复反馈,谢谢大家!
文章参考来源:MDN文档
本文为Tz张无忌文章,读后有收获可以请作者喝杯咖啡,转载请文章注明出处:https://www.cnblogs.com/zhaohongcheng/
深度剖析前端JavaScript中的原型(JS的对象原型)的更多相关文章
- 前端学习 第二弹: JavaScript中的一些函数与对象(1)
前端学习 第二弹: JavaScript中的一些函数与对象(1) 1.apply与call函数 每个函数都包含两个非继承而来的方法:apply()和call(). 他们的用途相同,都是在特定的作用域中 ...
- 深入理解JavaScript中创建对象模式的演变(原型)
深入理解JavaScript中创建对象模式的演变(原型) 创建对象的模式多种多样,但是各种模式又有怎样的利弊呢?有没有一种最为完美的模式呢?下面我将就以下几个方面来分析创建对象的几种模式: Objec ...
- javascript中,一个js中的函数,第一句var _this = this;为什么要这样做?
javascript中,一个js中的函数,第一句var _this = this;为什么要这样做? 下面是源码: 下面这段代码是常用的网站首页,自动切换span或者tabbar来变更List显示内容的 ...
- 透过一道面试题来探探JavaScript中执行上下文和变量对象的底
在做面试题之前,我们先搞清楚两个概念 执行上下文(execution context) 变量对象(variable object) 执行上下文 我们都知道JavaScript的作用域一共分三种 全局作 ...
- 前端设计中关于外部js文件加载的速度优化
在一般情况下,许多人都是将<script>写在了<head>标签中,而许多浏览器都是使用单一的线程来加载js文件的,从上往下,从左往右. 若是加载过程出错,那么网页就会阻塞,就 ...
- javascript中的this与prototype,原型理解
JavaScript 函数调用 JavaScript 函数有 4 种调用方式. 每种方式的不同方式在于 this 的初始化. this 关键字 一般而言,在Javascript中,this指向函数执行 ...
- Javascript中的函数、this以及原型
关于函数 在Javascript中函数实际上就是一个对象,具有引用类型的特征,所以你可以将函数直接传递给变量,这个变量将表示指向函数“对象"的指针,例如: function test(mes ...
- 在JavaScript中使用json.js:使得js数组转为JSON编码
在json的官网中下载json.js,然后在script中引入,以使用json.js提供的两个关键方法. 1.数组对象.toJSONString() 这个方法将返回一个JSON编码格式的字符串,用来表 ...
- javascript中如何使用js脚本模拟"request"获取url后参数值呢?
转自:猫猫小屋--js获取url后参数信息 摘要: 下文讲述javascript中使用js代码获取url地址后面的参数值的方法分享,如下所示: 实现思路: 使用正则表达式对参数值进行匹配,获取参数后的 ...
随机推荐
- latex中文支持ubuntu
latex安装: sudo apt install texlive-full 中文字体安装: sudo apt-get install latex-cjk-all 字体包中包含bsmi,bk ...
- 打造Worktile敏捷开发管理工具的思与惑
从2019年初,我们团队准备开发一款适合研发团队使用的敏捷开发管理工具,那时候我们也在思考,到底什么样的工具才算是优秀的研发管理工具,研发管理的场景.方法和流派有很多,市面上关于研发管理工具的产品也是 ...
- CSS3过渡结束监听事件,清除/修改表单元素的一些默认样式
document.querySelector('div').addEventListener('transitionEnd',function(){ console.log('过度结束') }) 如果 ...
- SQL Server中创建sde数据库
在ArcCatalog或者ArcMap中打开ArcToolBox工具箱. 在工具箱中,找到创建企业级地理数据库工具,依次为数据管理工具→地理数据库管理→创建企业级地理数据库,如图所示. 双击打开创建企 ...
- iOS 优化实例
一.接口请求优化 在工程项目中,多个一级界面包含状态,如:服务入口的动态配置,未读消息数量,图片文字等,因此产品设计要每次切换 tab 时都请求数据,及时的更新页面状态.在实际开发中,频繁的调用接口, ...
- 403 Invalid CORS request 跨域问题
5.跨域问题 跨域:浏览器对于javascript的同源策略的限制 . 以下情况都属于跨域: 跨域原因说明 示例 域名不同 www.jd.com 与 www.taobao.com 域名相同,端口不同 ...
- M - 诡异的楼梯 HDU - 1180(BFS + 在某个点等待一下 / 重复走该点)
M - 诡异的楼梯 HDU - 1180 Hogwarts正式开学以后,Harry发现在Hogwarts里,某些楼梯并不是静止不动的,相反,他们每隔一分钟就变动一次方向. 比如下面的例子里,一开始楼梯 ...
- debian10切换国内源
Debian 10.3 下载地址: http://mirrors.163.com/debian-cd/10.3.0-live/amd64/iso-hybrid/debian-live-10.3.0-a ...
- 记一次JAVA进程导致Kubernetes节点CPU飙高的排查与解决
一.发现问题 在一次系统上线后,我们发现某几个节点在长时间运行后会出现CPU持续飙升的问题,导致的结果就是Kubernetes集群的这个节点会把所在的Pod进行驱逐(调度):如果调度到同样问题的节点上 ...
- js之:漂浮线
(function initJParticle( $ ){ "use strict"; var createParticlesSandbox, Utils; Utils = {}; ...