美文网首页
关于JS原型和类的总结

关于JS原型和类的总结

作者: 景阳冈大虫在此 | 来源:发表于2020-04-15 16:17 被阅读0次
前言

众所周知,JavaScript是使用原型模式来搭建整个面向对象系统的。而
JavaScript中实际上没有类,类是一种可选择的设计模式而不像Java那样万物皆是类。如果之前使用过一段时间Java才学JS的可能看到这一大堆newprototypeinstanceof…脑子里会一片混沌。
因此,将这些与类和原型链相关的东西摆在一起统一梳理十分有必要。

提前说明,本篇除了MDN之外,还参考了《你不知道的JavaScript(上卷)》、《JavaScript设计模式与开发实践》,Vue源码。所用例子也大都出自上述来源,故本篇实则为一篇读书笔记。

类是一种设计模式。而提到类,会自然而然想到面向类的设计模式:实例化、继承和多态。

实例化

一个类就是一张蓝图。为了获得真正可以交互的对象,我们必须按照类来建造(也可以说实例化)一个东西,这个东西通常被称为实例,有需要的话,我们可以直接在实例上调用方法并访问其所有公有数据属性。
这个对象就是类中描述的所有特性的一份副本
知识点:类通过复制操作被实例化为对象形式。

提到实例化,第一想到的是关键字new

“实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。”

简化的new实现过程类似于这样:

function Person(name) {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name;
};
var objectFactory = function() {
    var obj = new Object(), // 1. 从 Object.prototype 上克隆一个空的对象
        Construstor = [].shift.call(arguments); // 取得外部传入的构造器,此例是 Person
    obj.__proto__ = Construstor.prototype; // 2. 指向正确的原型
    var ret = Construstor.apply(obj, arguments); // 3. 借用外部传入的构造器给 obj 设置属性
    return typeof ret === 'object' ? ret : obj; // 4. 确保构造器总是会返回一个对象
};
var a = objectFactory(Person, 'sven');
console.log(a.name); // 输出:sven
console.log(a.getName()); // 输出:sven
console.log(Object.getPrototypeOf(a) === Person.prototype);
// 输出:true

——来源于《JavaScript设计模式与开发实践》

看这个具体的例子会比较好理解。
首先克隆Object.prototype作为最终可能输出的对象obj,然后获取传入的构造函数Person,接下来修改obj的__proto__
然后调用apply,使用obj为上下文,使用new时传入的参数作为入参,执行Construstor,后者就是传入的构造函数。
如果构造函数返回的值为对象,则传出这个对象,否则将第一行创建的obj返回。

  • _proto_

这个例子里出现的__proto__还挺奇怪的,在MDN中有一个示例可以说明为啥会有这一步。当创建一个实例时,这个实例的__proto__与它的类的原型相等。

function test() {}
test.prototype.myname = function () {
    console.log('myname');
}
let a = new test()
console.log(a.__proto__ === test.prototype);//true
a.myname();//myname

__proto__实现大致是这样

Object.defineProperty( Object.prototype, "__proto__", { 
    get: function() {
        return Object.getPrototypeOf( this ); 
    },
    set: function(o) {
        // ES6中的setPrototypeOf(..)
        Object.setPrototypeOf( this, o );
        return o;
    } 
} );

这里用到的__proto__其实在MDN文档开始就再三强调了是个不推荐的概念,现在得用getPrototypeOfsetPrototypeOf代替对__proto__的操作

Object.getPrototypeOf( a ) === Foo.prototype;

“new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。”
摘录来自:“你不知道的JavaScript(上卷)。”

这个new知识点很重要,一定要记牢。

原型

1. prototype

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。

所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。

上面说到,new操作得到一个关联了其他对象的新对象。

原型继承

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。
委托这个术语可以更加准确地描述JavaScript中对象的关联机制。

  • constructor
function Foo() { /* .. */ }

Foo.prototype.constructor === Foo; // true

var a = new Foo(); 
a.constructor === Foo; // true”

函数的原型的构造器等于它自身。

Foo.prototype = { /* .. */ }; // 创建一个新原型对象

var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!”

看起来是Foo()构造了a1,但实际上并不是这样的。
到底怎么回事?a1并没有.constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype。但是这个对象也没有.constructor属性(不过默认的Foo.prototype对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的Object.prototype。这个对象有.constructor属性,指向内置的Object(..)函数。

因此可以解释,当手动修改了prototype之后,若没有重新添加constructor并对其赋值,构造器constructor并不存在。

实际上,对象的.constructor会默认指向一个函数,这个函数可以通过对象的.prototype引用。“constructor”和“prototype”这两个词本身的含义可能适用也可能不适用。
最好的办法是记住这一点“constructor并不表示被构造”。

2. 继承

再回去看上图(原型继承),它不仅展示出对象(实例)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”

这段代码用原型风格表现了继承。
可以先回顾一下上面提到的MDN里给的那个例子,当new一个实例后,实例a与创建它的构造函数Foo的Foo.prototype就存在了委托关系

Object.getPrototypeOf( a ) === Foo.prototype;

在这个继承例子里,Bar到Foo也有委托关系,是通过Object.create去实现的,那么Object.create做了什么呢?

Bar.prototype = Object.create( Foo.prototype );
  • Object.create

调用Object.create(..)会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象(本例中是Foo.prototype)。
——你不知道的JavaScript

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
——MDN

换句话说,这条语句的意思是:“创建一个新的Bar.prototype对象并把它关联到Foo.prototype”。

下面这两条例子接近但与Object.create并不等同

// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;

// 基本上满足你的需求,但是可能会产生一些副作用 :( 
Bar.prototype = new Foo();

Bar.prototype = Foo.prototype并不会创建一个关联到Bar.prototype的新对象,它只是让Bar.prototype直接引用Foo.prototype对象。
如果对Bar.prototype进行修改,因为引用的关系,实际上会导致Foo.prototype发生变更。

Bar.prototype = new Foo()的确会创建一个关联到Bar.prototype的新对象。但是new会使用Foo()的构造函数调用,如果Foo()的构造函数干了点其他的事情(在构造函数里做点什么非常正常)。
后果是,我们只是想将Bar.prototypeFoo.prototype关联起来,却极有可能因为new操作干了很多额外的我们无从预料的事情。

Object.create 的polyfill如下

if(!Object.create){
  Object.create=function(o){
    var F=function(){};
    F.prototype=o;
    return new F();
  };
}

Object.create(null)
Object.create(null)会创建一个拥有空(或者说null)[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof操作符(之前解释过)无法进行判断,因此总是会返回false
这些特殊的空[[Prototype]]对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据
这个特殊的用法值得被了解,在vue-router中可以找到它的应用如下:

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
…………
}
存储数据

Obeject.create唯一的缺点是需要创建一个新对象然后把旧对象抛弃掉。
这句话怎么理解呢?
因为Bar.prototype对象默认是与Object.prototype关联的,可以理解为原先是Obeject.create(Object.prototype)。使用这个方法,不是对Bar.prototype的修改,而是将原先的对象丢弃,给Bar.prototype重新赋值以新创建的Obeject.create(Foo.prototype)。这一操作可能会造成轻微的性能损失。因为抛弃的对象需要垃圾回收。

  • Object.setPrototypeOf( Bar.prototype, Foo.prototype )

如果能有一个标准并且可靠的方法来修改对象的[[Prototype]]关联就好了。在ES6之前,我们只能通过设置.__proto__属性来实现,但是这个方法并不是标准并且无法兼容所有浏览器。
ES6添加了辅助函数Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。

// ES6之前需要抛弃默认的Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );

// ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

看到这里,再回去看开头用代码描述的new过程,其实有一些语句在ES6的时代都不算是最精确的。按照《你不知道的JavaScript(上卷)》与MDN里的描述,应该改成这样

function Person(name) {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name;
};
var objectFactory = function() {
    var obj = new Object(); // 1. 从 Object.prototype 上克隆一个空的对象
        Construstor = [].shift.call(arguments); // 取得外部传入的构造器,此例是 Person
    Object.setPropertypeOf(obj,Construtor.prototype); // 2. 指向正确的原型
    var ret = Construstor.apply(obj, arguments); // 3. 借用外部传入的构造器给 obj 设置属性
    return typeof ret === 'object' ? ret : obj; // 4. 确保构造器总是会返回一个对象
};
var a = objectFactory(Person, 'sven');

3. 检查“类”关系
假设有对象a,如何寻找对象a委托的对象?

function Foo() { 
    // ...
}

Foo.prototype.blah = ...;

var a = new Foo();
  • instanceof
    instanceof操作符的左操作数是一个普通的对象,右操作符是一个函数。
    instanceof回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象?
a instanceof Foo; // true

有一个很常见的误解:用instanceof去判定左边是否为右边的实例。
另:instanceof不能直接判断两个对象是否有关联关系,所以在这个例子里实现了这样的辅助函数。

// 用来判断o1是否关联到(委托)o2的辅助函数 
function isRelatedTo(o1, o2) {
    function F(){} 
    F.prototype = o2; 
    return o1 instanceof F;
} 

var a = {};
var b = Object.create( a ); 

isRelatedTo( b, a ); // true

在这个例子里,bObject.create( a )的结果。我们上面提到过,这个的结果是b.prototype会关联到a
显然,b并不是调用了F构造出来的实例,但在isRelatedTo函数里,用instanceof来判定这两者得到的答案是true

isPrototypeOf()instanceof运算符不同。在表达式 "object instanceof AFunction"中,object 的原型链是针对 AFunction.prototype 进行检查的,而不是针对 AFunction 本身。
—— MDN instanceof

  • isPrototypeOf

isPrototypeOf()方法用于测试一个对象是否存在于另一个对象的原型链上。

Foo.prototype.isPrototypeOf( a ); // true

这个方法的好处在于我们可以直接用两个对象判定,而不是传入了函数但用来判定的却是这个函数的prototype

相关文章

  • 关于JS原型和类的总结

    前言 众所周知,JavaScript是使用原型模式来搭建整个面向对象系统的。而JavaScript中实际上没有类,...

  • 5-1 从基础知识到JSWebAPI

    回顾js基础知识 JS-web-API 总结 回顾js基础知识 变量类型和计算 原型和原型链 闭包与作用域 异步和...

  • web2

    js是面向对象的语言 是因为js的高度动态性的对象系统 面对对象变成:基于类 和 基于原型 js属于基于原型 性能...

  • 关于JS中的原型和原型链

    目录 关于js 对象和原型 原型链 基于原型链的继承 参考资料ECMAScript 6 入门JavaScript原...

  • js 主流创建类操作

    JS 创建类:混合的构造函数/原型方式 动态原型创建 新的Class - 类创建

  • JavaScript进阶:原型模式

    1、前言 原型模式是指的将原型对象指向创建对象的类,使得不同的类共享原型对象的方法和属性。js中基于原型链的继承的...

  • 关于js原型链和类继承的理解

    了解过 javascript 的人基本都知道JS继承的其它语言的继承是有些不一样的,其它的语言的继承是基于类的继承...

  • 谈谈js原型和原型链

    谈到原型和原型链,相信很多人都会想到js类与继承,但由于js是基于对象的语言,并不是面向对象的,所以它本身并没有类...

  • JS继承的几种方式

    关于Js继承的几种方式,总结一下,以便查看。 第一种 prototype 引用型原型继承 语言支持:js原生支持的...

  • javascript的六种继承方式

    js中常用的继承方式:子类继承父类的方法 1、原型继承 子类的原型指向父类的一个实例 父类的私有和公有方法都是子类...

网友评论

      本文标题:关于JS原型和类的总结

      本文链接:https://www.haomeiwen.com/subject/otknmhtx.html