资讯专栏INFORMATION COLUMN

写给Java开发者看的JavaScript对象机制

Charles / 2057人阅读

摘要:如果你已经对机制已有了解,但是由于两者对象机制的巨大本质差异,对它和构造函数,实例对象的关系仍有疑惑,本文或许可以解答你的问题。所有的原型对象都会自动获得一个属性,这个属性的值是指向原型所在的构造函数的指针。

帮助面向对象开发者理解关于JavaScript对象机制

本文是以一个熟悉OO语言的开发者视角,来解释JavaScript中的对象。

对于不了解JavaScript 语言,尤其是习惯了OO语言的开发者来说,由于语法上些许的相似会让人产生心理预期,JavaScript中的原型继承机制和class语法糖是让人迷惑的。

如果你已经对prototype机制已有了解,但是由于两者对象机制的巨大(本质)差异,对它和构造函数,实例对象的关系仍有疑惑,本文或许可以解答你的问题。

我们看下面的代码,可以看出和OO语言相比,语法上也有很大分别:

// 定义一个类
class Foo {
  constructor() {
    this.a = "a";
  }
}

//实例化对象
const foo = new Foo();

//定义原型的属性
Foo.prototype.b = "b";

//实例可以访问属性
foo.b // "b"

//修改原型的属性
Foo.prototype.b= "B";


//实例属性值没有被修改
foo.b // "b"

类已经定义了怎么还能修改呢?prototype又是什么?

不存在面向对象

对于熟悉了面向对象的开发者而言JS中种种非预期操作的存在,都是因为JavaScript中根本没有面向对象的概念,只有对象,没有类。

即使ES6新添了class语法,不意味着JS引入了面向对象,只是原型继承的语法糖。

原型是什么

什么是原型?如果说类是面向对象语言中对象的模版,原型就是 JS中创造对象的模版。

在面向类的语言中,实例化类,就像用模具制作东西一样。实例化一个类就意味着“把类的形态复制到物理对象中”,对于每一个新实例来说都会重复这个过程。

但是在JavaScript中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。

//构造函数
function Foo(){
}
//在函数的原型上添加属性
Foo.prototype.prototypeAttribute0 = {status: "initial"};

const foo0 = new Foo();
const foo1 = new Foo();
foo0.prototypeAttribute0 === foo1.prototypeAttribute0 //true
对象、构造函数和原型的关系

当我们创建一个新对象的时候,发生了什么,对象、构造函数和原型到底什么。

先简单地概括:

原型用于定义共享的属性和方法。

构造函数用于定义实例属性和方法,仅负责创造对象,与对象不存在直接的引用关系。

我们先不用class语法糖,这样便于读者理解和暴露出他们之间真正的关系。

// 先创建一个构造函数 定义原型的属性和方法
function Foo() {
    this.attribute0 = "attribute0";
}

当创建了一个函数,就会为该函数创建一个prototype属性,它指向函数原型。

所有的原型对象都会自动获得一个constructor属性,这个属性的值是指向原型所在的构造函数的指针。

现在定义原型的属性和方法

Foo.prototype.prototypeMethod0 = function() {
    console.log("this is prototypeMethod0");
}

Foo.prototype.prototypeAttribute0 = "prototypeAttribute0";

好了,现在,新建一个对象,

const foo = new Foo();

foo.attribute0 // "attribute0"
foo.prototypeAttribute0 //"prototypeAttribute0"
foo.prototypeMethod0() // this is prototypeMethod0

它拥有自己的实例属性attribute0,并且可以访问在原型上定义的属性和方法,他们之间的引用关系如图所示。

当调用构造函数创建实例后,该实例的内部会包含一个指针(内部对象),指向构造函数的原型对象。

当读取实例对象的属性时,会在实例中先搜寻,没有找到,就会去原型链中搜索,且总是会选择原型链中最底层的属性进行访问。

对象的原型可以通过__proto__在chrome等浏览器上访问。

__proto__是对象的原型指针,prototype是构造函数所对应的原型指针。

语法糖做了什么

ES6推出了class语法,为定义构造函数和原型增加了便利性和可读性。

class Foo {
    constructor(){
        this.attribute0 = "attribute0";
    }

    prototypeMethod0(){
        console.log("this is prototypeMethod0")
    }
}

/* 相当于下面的声明*/
function Foo() {
    this.attribute0 = "attribute0";
}

Foo.prototype.prototypeMethod0 = function() {
    console.log("this is prototypeMethod0")
}

class中的constractor相当于构造函数,而class中的方法相当于原型上的方法。、

值得注意的特性 属性屏蔽 —— 避免实例对象无意修改原型

看这段代码,思考输出的结果。

class Foo {
    prototypeMethod0(){
        console.log("this is prototypeMethod0")
    }
}

const foo0 = new Foo();
const foo1 = new Foo();

foo0.prototypeMethod0 === foo0.__proto__.prototypeMethod0 // true

foo0.prototypeMethod0 = () => console.log("foo0 method");
foo0.prototypeMethod0(); //??
foo1.prototypeMethod0(); //??
foo0.prototypeMethod0 === foo0.__proto__.prototypeMethod0 // ??

输出的结果是

foo0.prototypeMethod0(); // foo0 method
foo1.prototypeMethod0(); // this is prototypeMethod0
foo0.prototypeMethod0 === foo0.__proto__.prototypeMethod0 // false

我们知道对象(即便是原型对象),都是运行时的。

创建之初,foo本身没有prototypeMethod0这个属性,访问foo0.prototypeMethod0将会读取foo0.__proto__.prototypeMethod0

直接修改foo0.prototypeMethod0没有改变__proto__上的方法原因是存在属性屏蔽

现在的情况是:想要修改foo0.prototypeMethod0prototypeMethod0foo中不存在而在上层(即foo.__proto__中存在),并且这不是一个特殊属性(如只读)。

那么会在foo中添加一个新的属性。

这便是为什么直接修改却没有影响__proto__的原因。

小结

再温习一遍这些定义:

原型用于定义共享的属性和方法。

构造函数用于定义实例属性和方法,仅负责创造对象,与对象不存在直接的引用关系。

__proto__是对象的原型指针,prototype是构造函数的原型指针。

在解释原型作用的文章或书籍中,我们会听到继承这样的术语,其实更准确地,委托对于JavaScript中的对象模型来说,是一个更合适的术语。

委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象。对象之间的关系不是复制而是委托。

参考

《JavaScript高级程序设计》

《你不知道的JavaScript》

本文仅供解惑,要在脑袋里形成系统的概念,还是要看书呀。

有疑问欢迎大家一起讨论。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108193.html

相关文章

  • Java开发

    摘要:大多数待遇丰厚的开发职位都要求开发者精通多线程技术并且有丰富的程序开发调试优化经验,所以线程相关的问题在面试中经常会被提到。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析 Java - 注解详解 详细介绍 Java 注解的使用,有利于学习编译时注解 Java 程序员快速上手 Kot...

    Lucky_Boy 评论0 收藏0
  • Java开发

    摘要:大多数待遇丰厚的开发职位都要求开发者精通多线程技术并且有丰富的程序开发调试优化经验,所以线程相关的问题在面试中经常会被提到。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析 Java - 注解详解 详细介绍 Java 注解的使用,有利于学习编译时注解 Java 程序员快速上手 Kot...

    LuDongWei 评论0 收藏0
  • 写给小白看的JS异步

    摘要:主线程在启动程序时被创建,用于执行函数。用户自主创建的若干进程相对于主线程而言就是子线程。子线程和主线程都是独立的运行单元,各自的执行互不影响,因此能够并发执行。这就是的异步机制了。 某天突然写了个方法要从后台调用数据,显示在前台页面,但是输出结果总是空undefined,得不到数据。多方找资料才发现,原来是入了JS异步的坑。 我们常常听到单线程、多线程、同步、异步这些概念,那么这些东...

    gghyoo 评论0 收藏0
  • 26自学转行前端(写给和1年前一样迷茫的我的你)

    摘要:转行前端有哪些疑虑在人生的抉择处,寻求一些别人的经验和总结,无可厚非,但是决定了就一定要坚定的走下去,谨慎是为了更好的坚持,而不是放弃的理由。写在前面这里前后端指的是开发的前后端。 转行前端有哪些疑虑? 在人生的抉择处,寻求一些别人的经验和总结,无可厚非,但是决定了就一定要坚定的走下去,谨慎是为了更好的坚持,而不是放弃的理由。写在前面:这里前后端指的是web开发的前后端。1、前端岗位需...

    番茄西红柿 评论0 收藏2637
  • 26自学转行前端(写给和1年前一样迷茫的我的你)

    摘要:转行前端有哪些疑虑在人生的抉择处,寻求一些别人的经验和总结,无可厚非,但是决定了就一定要坚定的走下去,谨慎是为了更好的坚持,而不是放弃的理由。写在前面这里前后端指的是开发的前后端。 转行前端有哪些疑虑? 在人生的抉择处,寻求一些别人的经验和总结,无可厚非,但是决定了就一定要坚定的走下去,谨慎是为了更好的坚持,而不是放弃的理由。写在前面:这里前后端指的是web开发的前后端。1、前端岗位需...

    番茄西红柿 评论0 收藏2577

发表评论

0条评论

Charles

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<