# prototype和__proto__分析

来源:互联网 发布:7.0wow mac 插件 编辑:程序博客网 时间:2024/06/16 11:37

标签(空格分隔): JAVASCRIPT DEEP


本文总结js里不太好理解的几个概念:prototype, __proto__, new, Object.create, instanceof, typeof。


对象和实例

先来说明实例和对象,简单来说对象就是一个概念(抽象的),实例就是物体(实体)。
下面直接上代码:

// 为了区分对象和原型,下面所有的对象统统用大写function A(name,age){this.name=name; this.age=age;}a = new A("test", 10);console.log(a.name, a.age); // => "test" 10

a就是一个对象,通过new这个关键字把实例变成了一个实例。new后面再说,这里先只考虑prototype
在js里,几乎一切都是实例,而并非一切都是对象,可以简单地认为有prototype这个属性的都是对象。

这里只是粗略地说法,比如你强行给一个变量设置一个prototype属性,其仍然不是一个对象,其仍然不能执行new操作。只能做99%的情况下,如果有prototype的属性,就可以认为它是一个对象。

对象可以被实例化,对象可以被继承。对象本身也是实例,其只不过是被更高层的对象实例化出来的而已。
说到底prototype也无非就是对象的一个属性而已。这是一个特殊的属性,我们可以简单理解为该属性里放着对象A专门给其实例和后代的实例准备的内容。反之,只有prototype里面的内容才会继承给实例和子对象,A本身的方法并不会被继承。对于一个对象,其显式地通过A.prototype.someFunc来调用prototype属性里的内容,而对于一个实例,则可以直接采用a.someFunc的方式调用其中的内容。
另外,为了方便开发者访问实例的对象的prototype属性,很多浏览器都实现了__proto__这个好用的关键字。在js中,任何内容都有__proto__这一属性, 这也从侧面应证了之前说的js里几乎一切都是实例;不过也有例外,比如数字、字符串等基本类型。虽然"" instanceof Object等会返回false, 按说应该没有更底层的对象了。但是这些基础变量也都有__proto__属性,对应到了合适的类型上,这么做可能主要是为了使用方便吧。需要注意的是,__prot__并非js标准,有些场景下可能会报错。
下面我们来具体看一下prototype属性的特点。代码如下:

function A(){};A.prototype.test = () => console.log("A.prototype.test");let a = new A();A.prototype.test(); // => A.prototype.test//A.test(); // => Error...A.test = () => console.log("A.test");A.test() // => A.testa.test(); // A.prototype.testa.__proto__.test(); // A.prototype.testa.test = () => console.log("a.test");a.test(); // a.testa.__proto__.test(); // A.prototype.test

可以看到,对于对象A,它是无法直接通过A.test访问到·prototype里的内容的。对于实例a,如果有test则直接使用自己的test,如果没有找到则会去其对象的prototype(即__proto__)里找。(当然如果还没找到会继续找其父对象的prototype
到现在为止,基本可以理清prototype的含义了,基本就是一个特殊属性,主要是服务于对象和实例之间的联系以及对象之间的继承关系。


继承关系

上面分析了prototype属性的作用,但是只分析了其在对象和实例之间的纽带作用。而它更主要的作用是用于实现继承,js里的继承关系就是基于prototype实现的。尽管现在已经引入了class,extends等这些关键字,不过这仅仅是语法糖而已,实际还是通过prototype等关键字实现的。
开始之前,我们先确认一下怎么才算继承。
既然要继承,子对象肯定要有父对象所有的属性,而且子对象实例化出来的变量应该也是父对象的实例。
具体的实现方法可以参考廖雪峰的JS入门教程,这里给出另一种写法,本质是一样的。

function A(){}function B(props){    A.call(this,props);// ...}B.prototype = Object.create(A.prototype);// 修复B.prototype.constructor = B;

为啥非要通过F=>new F来转换一遍呢?
直接复制过去了PrimaryStudentStudent就没区别了啊,你想给PrimaryStudent加一个新方法,你会发现Student也有了该方法。
为啥非要修复最后一个constructor?
事实上,这一步即便不执行,在大部分场景下也不会出问题。为什么一定要保证prototype里的constructor指向对象本身呢?这类似C++里的多态,具体分析可以参考下面的链接Stack Overflow关于prototype.constructor的讨论。大致就是在基类中操作的某些通用函数需要知道处理谁。

这样做完以后,所有从B实例化出去的变量都能够直接使用A的方法,且都是A的实例化。

b = new B()b.__proto__ === B.prototype // trueb.__protot__.__proto__ === A.prototype // trueb instanceof B // trueb instanceof A // true

如下:

console.log(A.prototype);// => {constructor: f}//constructor是个函数?A.prototype.constructor === A // true  wtf?

我们发现对象的prototype属性有一个constructor属性等于对象本身。

一等公民——Function

很多文章里都会说,函数是js里的一等公民,之前也就简单理解为函数比较重要罢了,不过实际拿prototype__proto__试了一下发现,Function确实比较特殊。
具体看下面代码:

Object.__proto__ === Function.prototype; // trueFunction.__proto__ === Function.prototype; // trueFunction.__proto__ === Object.__proto__; // trueFunction.__proto__.__proto__ === Object.prototype; //trueFunction instanceof Object; // trueObject instanceof Function; // true

可以简单总结为

  • Function和Object都是Function的实例。
  • Function继承自Object。
  • Function是Object类型。
  • Object是Function类型。

这么设计肯定有原因的,至于具体原因这里就不深入探讨了,不过确实只有Function有这些属性。其他内置的类型,例如Number,Date之类的都没有这些等式。
从这里可以看到,函数在js里确实比较特殊,这也是为什么经常会看到函数被用作桥梁而其他的类型就不会。

对象扩展

有了prototype的加持,我们可以随意地对一个现有对象进行扩展,比如下面这段代码就是给Date加了一个format方法,功能与moment.js里的format类似。

Date.prototype["Format"] = function(fmt) {        fmt = fmt || "YYYY-MM-dd hh:mm:ss";        let o = {            "M+": this.getMonth() + 1, //月份            "d+": this.getDate(), //日            "h+": this.getHours(), //小时            "m+": this.getMinutes(), //分            "s+": this.getSeconds(), //秒            "q+": Math.floor((this.getMonth() + 3) / 3), //季度            S: this.getMilliseconds() //毫秒        };        if (/(y+)/.test(fmt))            fmt = fmt.replace(                RegExp.$1,                (this.getFullYear() + "").substr(4 - RegExp.$1.length)            );        for (let k in o)            if (new RegExp("(" + k + ")").test(fmt))                fmt = fmt.replace(                    RegExp.$1,                    RegExp.$1.length == 1                        ? o[k]                        : ("00" + o[k]).substr(("" + o[k]).length)                );        return fmt;    }}

有了这个扩展,我们就可以写出下面的代码了:

let d = new Date();console.log(d.format("yyyy-MM-dd:hh"));// => 2017-09-09:10console.log(Date.format("yyyy-MM-dd:hh"));// TypeError

类似地,我们同样可以对对象本身进行扩展:

JSON["safeParse"] = function(        text,        reviver    ) {        try {            return JSON.parse(text, reviver);        } catch (e) {            return null;        }    }}// parseJSON.parse("{x:")// => null

上面两个例子可以看到,我们需要分清应用的场景,需要针对需求进行扩展。
这么扩展的代价是什么?
这种扩展方式很简单粗暴,不过这也会给我们带来一些副作用(虽然大部分情况下都不会涉及)。
先看一下下面的代码:

// somewhere unkownObject.prototype.hello = () => console.log("hello");// doing sthlet a = {};a.name = "test";a.age = "21";a.roler = "adc";for(let k in a) {    console.log(a[k].toString());}// => test// => 21// => adc// => () => console.log("hello");

这里调用toString()主要是为了清晰地看到问题所在————我们扩展的属性会被in操作符所遍历,这个在数组上也会有类似的问题(不过数组对象可以使用of来避免这个问题,这也是为什么大部分教程里都会建议使用of来遍历数组的原因)。前面知道所有的对象都来自Object,所以对Object的扩展会影响到所有的in关键字!!!
那怎么解决呢?
为了避免这个问题,后面有了hasOwnProperty这个函数。我们只需要简单地加一行代码就行了。

for(let k in a) {    if(!a.hasOwnProperty(k))continue;    console.log(a[k].toString());}

in关键字存在的地方务必都加上这一判断。如果没有加这个关键字,即便现在程序没有出错,但是后续开发中一旦有人对关联的对象做了扩展,这块代码就有可能出错或者输出跟预期不符,可以设想把上面的hello函数换成Object.prototype.hello = "nevermore",这个时候程序不会报错,只是输出错了。这个时候就看可能出现一些诡异的bug,项目大的时候很难调试。

从上面我们可以看到,利用prototype进行扩展会给系统带来不小的隐患,尤其是对基础对象(Object, Funtiong, Array)进行扩展的时候,因为我们永远也没法保证其他人的代码都是安全的。

怎么解决这一隐患呢?
为了解决这一问题,又有了defineProperty这一函数。借用这个函数我们可以更加安全地扩展对象,这里简单地介绍一下,更加详细的内容请参考MDN文档
该函数原型如下:Object.defineProperty(obj, prop, descriptor)

  • obj: 我们需要扩展的对象,例如:Date.prototype
  • prop: 我们需要扩展的函数(或变量)名称,例如:”format”
  • descriptor: 设置这一新属性的特征。例如:{enumerable: false, value: () => console.log("hello")} 表示不允许该属性被枚举到,即不会被in之类的迭代器遍历到,且数值设置为一个函数。这个参数有下面6个配置项。
    • configurable: 是否允许修改或者删除属性特征。
    • enumerable: 是否允许迭代器遍历
    • value: 属性的值。
    • writable: 是否支持修改属性值。这里的修改和configurable的修改管理的内容不一样,这里是确定属性值是否允许修改,而上面是确认配置项是否允许修改。如果配置项允许修改我们可以再次调用defineProperty来把writable修改成true然后再修改该字段内容也是可以的。
    • get 见set
    • set get/set是一对gettersetter,它们是一套和value/writable互斥的配置,不能同时设置,否则会报错。其中getgetter完全一致,就是相当于吧该属性变成了一个gettersetter则会在该属性被修改的时候被调用。

关于上面讲到的get/set可以参考下面的代码:

let _am_ = 0;Object.defineProperty(Object.prototype, "game", {  get: function() { return Date.now() },  set: function(nV) { _am_ = nV + 1; },});let a = {};setInterval(()=>(a.game = a.game) && console.log(`game: ${a.game}`) || console.log(`_am_: ${_am_}`), 1000);// => game: 1504956090106// => _am_: 1504956090090// => game: 1504956091110// => _am_: 1504956091111

这里就展示get=>set的工作过程,每一次出现a.game=?的操作的时候就会触发set,可以尝试把上面的赋值操作去掉,就会发现set不会被触发(这个时候a.game还是会变化的)。
到此为止我们就可以优化之前对DateJSON扩展的代码了。这里仅以JSON扩展为例:

Object.defineProperty(JSON, "safeParse", {    value: function(        text: string,        reviver?: (key: any, value: any) => any    ): any {        try {            return JSON.parse(text, reviver);        } catch (e) {            return null;        }    }});

这么做是最安全的方案,不过如果运行环境不支持defineProperty则还是需要采用prototype关键字作为解决方案。

判断对象

我们经常会遇到需要判断参数的类型的场景,比如我们有一个下面的函数:

// 判断请求的token的格式handler = rgx => req => rgx.test(req.header("token"));handler(()=>{});// Error

为了安全起见,这个时候我们势必希望能够判断rgx的类型。在使用nodejs做服务的时候,类型判断就是最讨厌的问题之一。虽然js动态语言的性质导致了这一问题不可能完全解决,不过js还是提供了一些手段来做基本的类型判断。
typeof: typeof关键字会返回实例的对象,代码如下:

typeof function(){} // => "function"typeof 0 // => "number"typeof null // => "object"typeof undefined // => "undefined"typoef "" // => "string"typeof new RegExp(/^\d{11}$/) // => "object"

基本的类型typeof就可以确定了,不过上面的正则返回的是更底层的”object”。
instanceof: 该关键字就是用于判断类型的。

/\d+/g instanceof RegExp // => true/\d+/g instanceof Object // => true

关于instanceof的用法跟其他语言里基本一样,这里不再赘述。
除了上面两种写法,还有下面这种写法:

let typeOf = Object.prototype.toString;typeOf.call(/^\d{11}$/) // => [object RegExp]typeOf.apply(/^\d{11}$/) // => [object RegExp]

采用这种方法就可以找到一个对象更细致的类型了,具体哪些就不列了,有兴趣自己去尝试吧。关于call和apply的用法可以参考下面链接理解call和apply

原创粉丝点击