引言
最近又攀登了一下 JS 三座大山中的第二座。登山过程很酸爽,一路发现了许多之前没曾注意到的美景。本着独乐乐不如众乐乐的原则,这里和大家分享一下。
JS 的面试对象
有些人认为 JavaScript 不是真正的面向对象的语言,比如它没有像许多面向对象的语言一样有用于创建 class 类的声明
(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)
。JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。同理,原型链也是实现继承的主要方式(
ES6 的 extends 只是语法糖
)。
原型、原型链
一直在犹豫,到底是先讲创建对象的方法还是先讲原型。为了后面保证讲创建对象方法的连贯性,这里还是先讲讲原型吧,
这里为了权威,直接就摘抄MD
N 的定义了
JavaScript 常被描述为一种
基于原型的语言 (prototype-based language)
——每个对象拥有一个原型对象
,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)
,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。准确地说,这些属性和方法定义在 Object 的构造器函数 (constructor functions) 之上的 prototype 属性上,而非对象实例本身。
这个__proto__属性有什么用呢?在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制,而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的 prototype 属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
简单的说,就是实例对象能通过自己的__proto__属性去访问 “类”
原型 (prototype) 上的方法和属性, 类如果也是个实例,就会不断往上层类的原型去访问,直到找到
补充:
1.“类”的原型有一个属性叫做 constructor 指向“类”
2.__proto__已被弃用,提倡使用 Object.getPrototypeOf(obj)
举例:
var arr = [1,2,3] //arr 是一个实例对象(数组类 Array 的实例)arr.__proto__ === Array.prototype //true 实例上都有一个__proto__属性,指向“类”的原型
Array.prototype.__proto__ === Object.prototype //true“类”的原型也是一个 Object 实例,那么就一定有一个__proto__属性,指向“类”object 的原型
这里补充一个知识点:
浏览器在在 Array.prototype 上内置了 pop 方法,在 Object.prototype 上内置了 toString 方法
上图是我画的一个原型链图
[1,2,3].pop() //3
[1,2,3].toString() //'1,2,3'
[1,2,3].constructor.name //"Array"
[1,2,3].hehe() //[1,2,3].hehe is not a function
当我们调用 pop()的时候,在实例 [1,2,3] 上面没有找到该方法,则沿着原型链搜索 ” 类 ”Array 的原型,找到了 pop 方法并执行,同理调用 toString 方法的时候,在 ” 类 ”Array 没有找到则会继续沿原型链向上搜索 ” 类 ”Object 的原型,找到 toString 并执行。
当执行 hehe 方法的时候,由于“类”Object 的原型上并没有找到,搜索“类”Object 的__proto__, 由于执行 null, 停止搜索,报错。
注意,[1,2,3].constructor.name 显示‘Array’不是说明实例上有 constructor 属性,而是正是因为实例上没有,所以搜索到
类的原型上了,找到了 constructor
类,创建对象的方法
怎么创建对象,或者说怎么模拟类。这里我就不学高程一样,给大家介绍 7 种方法了,只讲我觉得必须掌握的。毕竟都 es6 es7 了,很多方法基本都用不到,有兴趣自己看高程。
利用构造函数
const Person = function (name) {
this.name = name
this.sayHi = function () {alert(this.name)
}
}
const xm = new Person('小明')
const zs = new Person('张三')
zs.sayHi() //'张三'
xm.sayHi() //'小明'
缺点:每次实例化都需要复制一遍函数到实例里面。但是不管是哪个实例,实际上 sayHi 都是相同的方法,没必要每次实例化的时候都复制一遍,增加额外开销。
寄生构造函数模式
function specialArray() {var arr = new Array()
arr.push.apply(arr,arguments)
arr.sayHi = function () {alert('i am an specialArray')
}
return arr
}
var arr = new specialArray(1,2,3)
这个和在数组的原型链上增加方法有啥区别?
原型链上增加方法,所有数组都可以用。寄生构造函数模式只有被 specialArray 类 new 出来的才能用。
组合使用原型和构造函数
// 共有方法挂到原型上
const Person = function () {this.name = name}
Person.prototype.sayHi = function () {alert(this.name)
}
const xm = new Person('小明')
const zs = new Person('张三')
zs.sayHi() //'张三'
xm.sayHi() //'小明'
缺点:基本没啥缺点了,创建自定义类最常见的方法,动态原型模式
也只是在这种混合模式下加了层封装,写到了一个函数里面,好看一点,对提高性能并没有卵用。
es6 的类
es6 的‘类’class 其实就是语法糖
class Person {constructor(name) {this.name = name}
say() {alert(this.name)
}
}
const xm = new Person('小明')
const zs = new Person('张三')
zs.sayHi() //'张三'
xm.sayHi() //'小明'
在 es2015-loose 模式下用 bable 看一下编译
"use strict";
var Person =
/*#__PURE__*/
function () {function Person(name) {this.name = name;}
var _proto = Person.prototype;
_proto.say = function say() {alert(this.name);
};
return Person;
}();
分析:严格模式,高级单例模式封装了一个类,实质就是 组合使用原型和构造函数
JS 世界里的关系图
知识点:
- Object.getPrototypeOf(Function) === Function.prototype // Function 是 Function 的实例,没毛病
- Object.getPrototypeOf(Object.prototype)
- 任何方法上都有 prototype 属性以及__proto__属性
任何对象上都有__proto__属性 - Function.__proto__.__proto__===Object.prototype
- Object.getPrototypeOf(Object)===Function.prototype
- 最高级应该就是 Function.prototype 了,因为 5
判断类型的方法
之前在 JS 核心知识点梳理——数据篇里面说了一下判断判断类型的四种方法,这里借着原型再来分析一下
1. typeof:
只能判断基础类型中的非 Null, 不能判断引用数据类型(因为全部为 object)它是操作符
2. instanceof:
用于测试构造函数的 prototype 属性是否出现在对象的原型链中的任何位置 风险的话有两个
// 判断不唯一
[1,2,3] instanceof Array //true
[1,2,3] instanceof Object //true
// 原型链可以被改写
const a = [1,2,3]
a.__proto__ = null
a instanceof Array //false
仿写一个 instanceof,并且挂在 Object.prototype 上,让所有对象都能用
// 仿写一个 instance 方法
Object.prototype.instanceof = function (obj) {
let curproto = this.__proto__
while (!Object.is(curproto , null)){if(curproto === obj.prototype){return true}
curproto = curproto.__proto__
}
return false
}
[1,2,3].instanceof(Array) //true
[1,2,3].instanceof(Object) //true
[1,2,3].instanceof(Number) //false
[1,2,3].instanceof(Function) //false
1..instanceof(Function) //false
(1).instanceof(Number) //true
3. constructor:
constructor 这玩意已经介绍过了,“类”的原型执行 constructor 指向“类”
风险的话也是来自原型的改写
[1,2,3].constructor.name //'Array'
// 注意下面两种写法区别
Person.protorype.xxx = function // 为原型添加方法,默认 constructor 还是在原型里
Person.protorype = { // 原型都被覆盖了,没有 constructor 了,所要要手动添加,要不然 constructor 判断失效
xxx:function
constructor:Person
}
4.Object.prototype.toString.call(xxx)
试了下,好像这个方法也不是很准
null 可以用 object.is(xxx,null) 代替
Array 可以用 Array.isArray(xxx) 代替
Object.prototype.toString.call([1,2,3]) //"[object Array]"
Object.prototype.toString.call(function(){}) //"[object Function]"
Object.prototype.toString.call(1) //"[object Number]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call({}) //"[object Object]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(true) // 特别注意 特别注意 特别注意 "[object Object]"
Object.prototype.toString.call('string') // 特别注意 特别注意 特别注意 "[object Undefined]"
总结
参照各种资料,结合自己的理解,在尽量不涉及到继承的情况下,详细介绍了原型及其衍生应用。由于本人技术有限,如果有说得不对的地方,希望在评论区留言。