标签(空格分隔): 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
的时候可能会返回false,但是这些基础变量也都有__proto
属性,对应到了合适的类型上,这么做可能主要是为了使用方便吧。需要注意的是,__proto
并非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.test
a.test(); // A.prototype.test
a.__proto__.test(); // A.prototype.test
a.test = () => console.log("a.test");
a.test(); // a.test
a.__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来转换一遍呢?
直接复制过去了PrimaryStudent
和Student
就没区别了啊,你想给PrimaryStudent
加一个新方法,你会发现Student
也有了该方法。
为啥非要修复最后一个constructor?
事实上,这一步即便不执行,在大部分场景下也不会出问题。为什么一定要保证prototype
里的constructor
指向对象本身呢?这类似C++里的多态,具体分析可以参考下面的链接Stack Overflow关于prototype.constructor的讨论。大致就是在基类中操作的某些通用函数需要知道处理谁。
这样做完以后,所有从B实例化出去的变量都能够直接使用A的方法,且都是A的实例化。
b = new B()
b.__proto__ === B.prototype // true
b.__protot__.__proto__ === A.prototype // true
b instanceof B // true
b 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; // true
Function.__proto__ === Function.prototype; // true
Function.__proto__ === Object.__proto__; // true
Function.__proto__.__proto__ === Object.prototype; //true
Function instanceof Object; // true
Object 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:10
console.log(Date.format("yyyy-MM-dd:hh"));
// TypeError
类似地,我们同样可以对对象本身进行扩展:
JSON["safeParse"] = function(
text,
reviver
) {
try {
return JSON.parse(text, reviver);
} catch (e) {
return null;
}
}
}
// parse
JSON.parse("{x:")
// => null
上面两个例子可以看到,我们需要分清应用的场景,需要针对需求进行扩展。
这么扩展的代价是什么?
这种扩展方式很简单粗暴,不过这也会给我们带来一些副作用(虽然大部分情况下都不会涉及)。
先看一下下面的代码:
// somewhere unkown
Object.prototype.hello = () => console.log("hello");
// doing sth
let 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是一对
getter
和setter
,它们是一套和value/writable互斥的配置,不能同时设置,否则会报错。其中get
跟getter
完全一致,就是相当于吧该属性变成了一个getter
。setter
则会在该属性被修改的时候被调用。
关于上面讲到的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还是会变化的)。
判断对象
我们经常会遇到需要判断参数的类型的场景,比如我们有一个下面的函数:
// 判断请求的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