前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JavaScript原型与继承

JavaScript原型与继承

作者头像
用户1428723
发布于 2020-08-06 07:55:16
发布于 2020-08-06 07:55:16
55200
代码可运行
举报
文章被收录于专栏:代码与画家代码与画家
运行总次数:0
代码可运行

对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。


基于原型链的继承

继承属性

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 proto。但它不应该与构造函数 func 的 prototype 属性相混淆。被构造函数创建的实例对象的 [[prototype]] 指向 func 的 prototype 属性。Object.prototype 属性表示 Object 的原型对象。

这里演示当尝试访问属性时会发生什么:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 让我们从一个自身拥有属性a和b的函数里创建一个对象o:
let f = function() {
   this.a = 1;
   this.b = 2;
}
/* 这么写也一样
function f() {
  this.a = 1;
  this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}

// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
//  (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。

// 综上,整个原型链如下: 

// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4

console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined
继承方法

JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。

当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var o = {
  a: 2,
  m: function(){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// 当调用 o.m 时,'this' 指向了 o.

var p = Object.create(o);
// p是一个继承自 o 的对象

p.a = 4; // 创建 p 的自身属性 'a'
console.log(p.m()); // 5
// 调用 p.m 时,'this' 指向了 p
// 又因为 p 继承了 o 的 m 函数
// 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a'

在 JavaScript 中使用原型

下去,来仔细分析一下这些应用场景下, JavaScript 在背后做了哪些事情。

正如之前提到的,在 JavaScript 中,函数(function)是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype 。请注意,以下的代码是独立的(出于严谨,假定页面没有其他的JavaScript代码)。为了最佳的学习体验,我们强烈建议阁下打开浏览器的控制台(在Chrome和火狐浏览器中,按Ctrl+Shift+I即可),进入“console”选项卡,然后把如下的JavaScript代码复制粘贴到窗口中,最后通过按下回车键运行代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function doSomething(){}
console.log( doSomething.prototype );
// 和声明函数的方式无关,
// JavaScript 中的函数永远有一个默认原型属性。
var doSomething = function(){};
console.log( doSomething.prototype );

在控制台显示的JavaScript代码块中,我们可以看到doSomething函数的一个默认属性prototype。而这段代码运行之后,控制台应该显示类似如下的结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

我们可以给doSomething函数的原型对象添加新属性,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

可以看到运行后的结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

现在我们可以通过new操作符来创建基于这个原型对象的doSomething实例。使用new操作符,只需在调用doSomething函数语句之前添加new。这样,便可以获得这个函数的一个实例对象。一些属性就可以添加到该原型对象中。

请尝试运行以下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 );

运行的结果类似于以下的语句:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    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 中, 那么就会在doSomeInstancing 的 proto 的 proto 的 proto 中查找。然而, 这里存在个问题:doSomeInstancing 的 proto 的 proto 的 proto 其实不存在。因此,只有这样,在 proto 的整个原型链被查看之后,这里没有更多的 proto , 浏览器断言该属性不存在,并给出属性值为 undefined 的结论。

让我们在控制台窗口中输入更多的代码,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);

结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar
性能

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。下面给出一个具体的例子来说明它:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log(g.hasOwnProperty('vertices'));
// true

console.log(g.hasOwnProperty('nope'));
// false

console.log(g.hasOwnProperty('addVertex'));
// false

console.log(g.__proto__.hasOwnProperty('addVertex'));
// true

hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:Object.keys())

注意:检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。

结论

在编写使用复杂代码之前,理解原型继承模型是至关重要的。此外,请注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题。此外,原生原型不应该被扩展,除非它是为了与新的 JavaScript 特性兼容。

示例

B 继承自 A:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function A(a){
  this.varA = a;
}

// 以上函数 A 的定义中,既然 A.prototype.varA 总是会被 this.varA 遮蔽,
// 那么将 varA 加入到原型(prototype)中的目的是什么?
A.prototype = {
  varA : null,
/*
既然它没有任何作用,干嘛不将 varA 从原型(prototype)去掉 ? 
也许作为一种在隐藏类中优化分配空间的考虑 ?
https://developers.google.com/speed/articles/optimizing-javascript 
如果varA并不是在每个实例中都被初始化,那这样做将是有效果的。
*/
  doSomething : function(){
    // ...
  }
}

function B(a, b){
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB : {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething : { 
    value: function(){ // override
      A.prototype.doSomething.apply(this, arguments); 
      // call super
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 代码与画家 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Javascript ES6版本的4个基础用法
JS 的 ES6版本已经被各大浏览器广泛支持,很多前端框架也已经使用 ES6,并且还有 Babel 可以做兼容处理,所以ES6已经进入了应用阶段 如果您对 ES6 还不太熟悉,下面4个简单的基础用法可以帮助您快速了解ES6 01 使用 let 和 const 声明变量 在传统的 ES5 代码中,变量的声明有两个主要问题 (1)缺少块儿作用域的支持 (2)不能声明常量 ES6中,这两个问题被解决了,增加了两个新的关键字:let 和 const 块儿作用域使用 let var a = 1; if (tru
dys
2018/04/03
7970
ES6【笔记】
答: ES6是新一代的JS语言标准,规范了JS使用标准,对分JS语言核心内容做了升级优化,,新增了JS原生方法,使得JS使用更加规范,更加优雅,更适合大型应用的开发。学习ES6是成为专业前端正规军的必经之路。为什么要学习它。啥也别说,问就是ES6牛逼坏了,不学习ES6,没怎样,写起来比较累。
痴心阿文
2022/11/18
4270
ES6【笔记】
「译」ES6:参数默认值的实现细节
在这篇文章中我们会介绍另一个 ES6 的特性,带默认值的函数参数。正如我们将看到的,有一些微妙的案例。
Chor
2019/11/07
5060
[javascript] 主流浏览器对ES6的支持情况
现在使用的js语法,基本是ES5的规范 ,15年出的ES6的规范增加了很多其他语法,要看浏览器的支持情况,如果浏览器不支持那么就会报错
唯一Chat
2020/04/24
4.2K0
ES6常用语法糖(附Babel配置使用方法)
获取数据: 解构赋值 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>解构赋值</title
zhaoolee
2018/04/19
1.2K0
ES6常用语法糖(附Babel配置使用方法)
前端MVC Vue2学习总结(七)——ES6与Module模块化、Vue-cli脚手架搭建、开发、发布项目与综合示例
使用vue-cli可以规范项目,提高开发效率,但是使用vue-cli时需要一些ECMAScript6的知识,特别是ES6中的模块管理内容,本章先介绍ES6中的基础与模块化的内容再使用vue-cli开发vue项目。 一、ECMAScript6概要 ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会,英文名称是European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为JavaSc
张果
2018/03/30
1.8K0
前端MVC Vue2学习总结(七)——ES6与Module模块化、Vue-cli脚手架搭建、开发、发布项目与综合示例
JavaScript 设计模式学习第四篇-ES6 中可能遇到的知识点
ES6(ECMAScript 6,ES2015)原来指的是 ECMA 组织在 2015 年发布的 ECMAScript 2015 标准,以后发布的 ECMAScript 2016 对应 ES7,依此类推。今年发布的 ECMAScript 2019 标准对应的就是 ES10。相比于之后发布的这些版本,ES6 的改变幅度非常大,因此我们通常说的 ES6 广义上也包括 ES5 之后的所有更新。
越陌度阡
2020/11/26
4820
JavaScript 设计模式学习第四篇-ES6 中可能遇到的知识点
项目中如何使用babel6详解
由于浏览器的版本和兼容性问题,很多es6,es7的新的方法都不能使用,等到可以使用的时候,可能已经过去了很多年。Babel可以把es6,es7的新代码编译成兼容绝大多数的主流浏览器的代码。 本篇文章主要介绍在项目中如何安装配置和使用babel. 1.在项目下初始化 package.json $ npm init 2.在项目中安装babel $ npm install babel-cli --save-dev 3.安装babel插件 $ npm install babel-preset-xxxxxx --sa
用户1741436
2018/05/16
7630
如何在ES5与ES6环境下处理函数默认参数
如何在ES5与ES6环境下处理函数默认参数 函数默认值是一个很提高鲁棒性的东西(就是让程序更健壮) MDN关于函数默认参数的描述:函数默认参数允许在没有值或undefined被传入时使用默认形参。 ES5 使用逻辑或||来实现 众所周知,在ES5版本中,并没有提供的直接方法供我们我们处理函数默认值 所以只能够自己去增强函数的功能,一般会这么来做: function doSomething (name, age) { name = name
贾顺名
2018/06/20
5540
ES6的前世今生
1996 年 11 月,Netscape 创造了javascript并将其提交给了标准化组织 ECMA,次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。
创译科技
2019/09/03
9740
ES6的前世今生
前端模块化开发--ES6相关知识
Promise 是异步编程的一种解决方案,避免了传统的回调函数的层层嵌套,也就是常说的“回调地狱”。
MiChong
2020/09/24
5410
ES6新特性
由于ES6在一些低版本的浏览器上无法运行,需转成ES5之前的版本兼容,以下有几种方案可以自动转换
jinghong
2020/05/09
1K0
ES6新特性
ES6--变量的声明及解构赋值
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
奋飛
2019/08/15
9440
带你入门 JavaScript ES6 (一)
ES6: 是 ECMA国际组织于 2015 年 6 月 17 日发布的 ECMAScript 第六版,正式名为 ECMAScript 2015,通常被成为 ES6 或 ECMAScript 6。
柳公子
2018/09/17
5660
JavaScript第十二弹——ES6(上)
Hello大家好,最近我们也讲了不少JavaScript的知识了,今天再来点实用的吧,不管是在工作中还是面试中,ES6都是我们会遇到的一个东西,ES6呢,全称是ECMAScript2015,那么ECMAScript与JavaScript又是啥关系呢?ECMAScript是JavaScript的规格,JavaScript是ECMAScript的实现,ES6呢则是JavaScript的下一代标准。
萌兔IT
2019/07/25
5520
ES6特性之:参数默认值
作为一个开发者,跟进行业步伐是非常需要的,不能躺在现有的知识和经验温床上做美梦。JavaScript的ES2015标准(即我们说的ES6)在2016年已经被广泛应用了,还没开始使用的朋友,赶紧来磨一下枪吧。
一斤代码
2018/08/21
3750
深入理解ES6--对象、函数扩展
javascript引擎会在访问作用域中查找其同名变量;如果找到,则变量的值被赋给对象字面量的同名属性。
奋飛
2019/08/14
4550
ES6 函数的扩展
ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
全栈程序员站长
2022/07/21
3190
ES6
在cmd命令窗口初始化项目-y代表全部默认同意,就不用一次次按回车了。命令执行完成后,会在项目根目录下生产package.json文件。
楠楠
2018/09/11
2.8K0
前端学到什么程度可以找到工作(应届毕业生有什么优势)
前端工程师“Front-End-Developer”源自于美国。大约从2005年开始正式的前端工程师角色被行业所认 可,到了2010年,互联网开始全面进入移动时代,前端开发的工作越来越重要。 最初所有的开发工作都是由后端工程师完成的,随着业务越来越繁杂,工作量变大,于是我们将项目中 的可视化部分和一部分交互功能的开发工作剥离出来,形成了前端开发。 由于互联网行业的急速发展,导致了在不同的国家,有着截然不同的分工体制。 在日本和一些人口比较稀疏的国家,例如加拿大、澳洲等,流行“Full-Stack Engineer”,也就是我们通常 所说的全栈工程师。通俗点说就是一个人除了完成前端开发和后端开发工作以外,有的公司从产品设计 到项目开发再到后期运维可能都是同一个人,甚至可能还要负责UI、配动画,也可以是扫地、擦窗、写 文档、维修桌椅等等。 而在美国等互联网环境比较发达的国家项目开发的分工协作更为明确,整个项目开发分为前端、中间层 和后端三个开发阶段,这三个阶段分别由三个或者更多的人来协同完成。 国内的大部分互联网公司只有前端工程师和后端工程师,中间层的工作有的由前端来完成,有的由后端 来完成。 PRD(产品原型-产品经理) – PSD(视觉设计-UI工程师) – HTML/CSS/JavaScript(PC/移动端网页,实现网页端的视觉展示和交互-前端工程师)
全栈程序员站长
2022/07/31
1.3K0
前端学到什么程度可以找到工作(应届毕业生有什么优势)
相关推荐
Javascript ES6版本的4个基础用法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档