3月份几乎每天都能看到面试的人从我身边经过,前段时间同事聊面试话题提到了原型链,顿时激起了我在开始学习前端时很多心酸的回忆。第一次接触js的面向对象思想是在读《js高程设计》(红宝书)的时候,这部分内容卡了我整整一个多月,记得那会儿用了很笨的办法,我把这两个章节来回读了一遍又一遍,仍然不能完全理解,大部分是凭借机械记忆。因为入门的时候很喜欢红宝书,在差不多一年的自学时间里基础部分翻了将近10遍。当然,原型链也读了10遍。很遗憾,那会儿我觉得自己只掌握了50%。直到读了一个系列的书叫 《你不知道的javascript》,这本书神奇的叩开了我通往js学习之路的另一扇大门,简直颠覆了我对js之前的所有认识。尤其是上卷关于this、闭包、原型链继承的理解思想潜移默化的影响了我对这门语言的认知。我还记得这本书是我在北京的地铁里用kindle读完的,然后在博客里写了4篇读书笔记。对于原型链,我曾经很偏执的喜欢,后来在决定要转前端之后到杭州的一次面试,因为面试是在周末,跟一家做人工智能的公司技术负责人聊了将近两个小时,他给了我很多前端职业发展的中肯建议(初到杭州面试的那段时间真的得到了很多陌生人的指引跟帮助),纠正了我很多偏见的认知,至今我还记得他的花名。
原型链设计机制一直是大多数前端开发最难理解的部分,据说当初 Brendan Eich 设计之初不想引入类的概念,但是为了将对象联系起来,加入的C++ new的概念,但是new没有办法共享属性,就在构造函数里设置了一个prototype属性,这一设计理念成为了js跟其他面向对象语言不同的地方,同时也埋下了巨大的坑!
为了解决因为委托机制带来的各种各样的缺点及语法问题,es6之后引入的class,class的实质还是基于原型链封装的语法糖,但是却大大简化的前端开发的代码,也解决了很多历史遗留的问题,(这里并不想展开讨论)。但是,es6之后,原型链真的不需要被了解了吗?在知乎上有一篇被浏览了130多万的话题 :《面试一个5年的前端,却连原型链也搞不清楚,满口都是Vue,React之类的实现,这样的人该用吗?》曾经引起过热议。接下来我们就来聊聊js的原型链吧!
关于 new 操作符
在聊原型链之前,我想先聊聊new,这是一个经常会在面试中被问到的基础问题。怎么使用这里不详细介绍,只是提一下js里new的设计原理:
-
创建一个新对象;
-
让空对象的[[prototype]](IE9以下没有该属性,在js代码里写法为__proto__)成员指向了构造函数的prototype成员对象;
-
使用apply调用构造器函数,this绑定到空对象obj上;
-
返回新对象。
function NEW_OBJECT(Foo){
var obj={};
obj.__proto__=Foo.prototype;
obj.constructor=Foo;
Foo.apply(obj,arguments)
return obj;
}
构造函数的主要问题是,每个方法都要再每个实例上重新创建一遍,不同实例上的同名函数是不相等的。例如:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
alert(person1.sayName == person2.sayName); /*false*/
然而,创建两个完成同样任务的Function 实例的确没有必要,通过把函数定义转移到构造函数外部来解决这个问题。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。这时候,该原型链登场了!
原型
1:[[prototype]]
JavaScript 中的对象有一个特殊的[[Prototype]] 内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]] 属性都会被赋予一个非空的值。所有普通的[[Prototype]] 链最终都会关联到内置的Object.prototype。
当我们试图访问一个对象下的某个属性的时候,会在JS引擎触发一个GET的操作,首先会查找这个对象是否存在这个属性,如果没有找的话,则继续在prototype关联的对象上查找,以此类推。如果在后者上也没有找到的话,继续查找的prototype,这一系列的链接就被称为原型链。
2:prototype
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性
3:constructor
对象的.constructor 会默认关联一个函数,这个函数可以通过对象的.prototype引用,.constructor 并不是一个不可变属性。它是不可枚举的,但是它的值是可写的(可以被修改)。._ proto _ === .constructor.prototype
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象,并改写constructor
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
(原型)继承
四种写法的思考
1:A.prototype = B.prototype
这种方法很容易理解,A要继承B原型链属性,直接改写A的Prototype关联到B的prototype,但是,如果在A上执行从B继承过来的某一个属性或方法,例如:A.prototype.myName =…会直接修改B.prototype本身。
2:A.prototype = new B()
这种方式会创建关联到B原型上的新对象,但是由于使用构造函数,在B上如果修改状态、主车道其他对象,会影响到A的后代。
3:A.prototype = Object.create(B.prototype) (ES5新增)
Object.create()是个很有意思的函数,用一段简单的polyfill来实现它的功能:
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
Object.create(null) 会创建一个拥有空( 或者说null)[[Prototype]]链接的对象,这个对象因为没有原型链无法进行委托
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // 内部委托!
};
myObject.doCool(); // "cool!"
4:Object.setPrototypeOf( A.prototype, B.prototype ); (ES6新增)
深度剖析 instanceof,彻底理解原型链
在segementfault上有这么一道面试题:
var str = new String("hello world");
console.log(str instanceof String);//true
console.log(String instanceof Function);//true
console.log(str instanceof Function);//false
先把这道题放一边,我们都知道typeof可以判断基本数据类型,如果是判断某个值是什么类型的对象的时候就无能为力了,instanceof用来判断某个 构造函数 的prototype是否在要检测对象的原型链上。
function Fn(){};
var fn = new Fn();
console.log(fn instanceof Fn) //true
//判断fn是否为Fn的实例,并且是否为其父元素的实例
function Aoo();
function Foo();
Foo.prototype = new Aoo();
let foo = new Foo();
console.log(foo instanceof Foo); //true
console.log(foo instanceof Aoo); //true
//instanceof 的复杂用法
console.log(Object instanceof Object) //true
console.log(Function instanceof Function) //true
console.log(Number instanceof Number) //false
console.log(Function instaceof Function) //true
console.log(Foo instanceof Foo) //false
看到上面的代码,你大概会有很多疑问吧。有人将ECMAScript-262 edition 3中对instanceof的定义用代码翻译如下:
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
我们知道每个对象都有proto([[prototype]])属性,在js代码中用__proto__来表示,它是对象的隐式属性,在实例化的时候,会指向prototype所指的对象;对象是没有prototype属性的,prototype则是属于构造函数的属性。通过proto属性的串联构建了一个对象的原型访问链,起点为一个具体的对象,终点在Object.prototype。
Object instanceof Object :
// 区分左侧表达式和右侧表达式
ObjectL = Object, ObjectR = Object;
O = ObjectR.prototype = Object.prototype;
L = ObjectL.__proto__ = Function.prototype ( Object作为一个构造函数,是一个函数对象,所以他的__proto__指向Function.prototype)
// 第一次判断
O != L
// 循环查找 L 是否还有 __proto__
L = Function.prototype.__proto__ = Object.prototype ( Function.prototype是一个对象,同样是一个方法,方法是函数,所以它必须有自己的构造函数也就是Object)
// 第二次判断
O == L
// 返回 true
Foo instanceof Foo :
FooL = Foo, FooR = Foo;
// 下面根据规范逐步推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判断
O != L
// 循环再次查找 L 是否还有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判断
O != L
// 再次循环查找 L 是否还有 __proto__
L = Object.prototype.__proto__ = null
// 第三次判断
L == null
// 返回 false
理解了这两条判断的原理,我们回到刚才的面试题:
console.log(str.__proto__ === String.prototype); //true
console.log(str instanceof String);//true
console.log(String.__proto__ === Function.prototype) //true
console.log(String instanceof Function);//true
console.log(str__proto__ === String.prototype)//true
console.log(str__proto__.__proto__. === Function.prototype) //true
console.log(str__proto__.__proto__.__proto__ === Object.prototype) //true
console.log(str__proto__.__proto__.__proto__.__proto__ === null) //true
console.log(str instanceof Function);//false
总结以上,str的原型链是:
str ---> String.prototype ---> Function.prototype ---> Object.prototype
最后,提一个可以通用的来判断原始数据类型和引用数据类型的方法吧:Object.prototype.toString.call()
ps:在js中,valueOf跟toString是两个神奇的存在!!!
console.log(Object.prototype.toString.call(123)) //[object Number]
console.log(Object.prototype.toString.call('123')) //[object String]
console.log(Object.prototype.toString.call(undefined)) //[object Undefined]
console.log(Object.prototype.toString.call(true)) //[object Boolean]
console.log(Object.prototype.toString.call({})) //[object Object]
console.log(Object.prototype.toString.call([])) //[object Array]
console.log(Object.prototype.toString.call(function(){})) //[object Function]
最后提一下js中不伦不类的class
面向委托 VS 类:
我觉得可能毕竟面向对象的很多语言都有类,而js的继承很多学习过其他语言的摸不着头脑,就导致了js一直向模仿类的形式发展,es6就基于原型链的语法糖封装了一个不伦不类的class,让人以为js实际上也有类,真得是为了让类似学习过java的朋友容易理解,狠起来连自己都骗!我很同意你不知道的javascript作者对于js中封装类的看法:ES6 的class 想伪装成一种很好的语法问题的解决方案,但是实际上却让问题更难解决而且让JavaScript 更加难以理解。
这两个的区别我并不想说太多,因为实际上我对类的理解也不多,只知道它的思想是定义好一个子类之后,相对于父类来说它就是一个独立并且完全不同的类。子类会包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新行为。子对父是真正的复制。
而在js中没有真正意思的复制,实质上都是基于一个委托机制,复制的只是一个引用(类似C语言中指针的理解,js高程中习惯用指针思维来解释,不过我更喜欢你不知道的javascript中的委托机制的说法。)
class的用法不再提,写到这里,已经写的很累了,尽管在一年前写过类似的文章,但是重新整理起来还是不太轻松的一件事,而且我现在也觉得对于JS的类理解的不是那么透彻,以后再慢慢深入理解吧!
参考文献:
1: JS高程设计 第六章
2: 你不知道的JavaScript(上卷)
3: JavaScript instanceof 运算符深入剖析
4: Javascript中一个关于instanceof的问题