es6以后,真的不须要知道原型链了吗?

 

3月份几乎天天都能看到面试的人从我身边通过,前段时间同事聊面试话题提到了原型链,顿时激起了我在开始学习前端时不少心酸的回忆。第一次接触js的面向对象思想是在读《js高程设计》(红宝书)的时候,这部份内容卡了我整整一个多月,记得那会儿用了很笨的办法,我把这两个章节来回读了一遍又一遍,仍然不能彻底理解,大部分是凭借机械记忆。由于入门的时候很喜欢红宝书,在差很少一年的自学时间里基础部分翻了将近10遍。固然,原型链也读了10遍。很遗憾,那会儿我以为本身只掌握了50%。直到读了一个系列的书叫 《你不知道的javascript》,这本书神奇的叩开了我通往js学习之路的另外一扇大门,简直颠覆了我对js以前的全部认识。尤为是上卷关于this、闭包、原型链继承的理解思想潜移默化的影响了我对这门语言的认知。我还记得这本书是我在北京的地铁里用kindle读完的,而后在博客里写了4篇读书笔记。对于原型链,我曾经很偏执的喜欢,后来在决定要转前端以后到杭州的一次面试,由于面试是在周末,跟一家作人工智能的公司技术负责人聊了将近两个小时,他给了我不少前端职业发展的中肯建议(初到杭州面试的那段时间真的获得了不少陌生人的指引跟帮助),纠正了我不少偏见的认知,至今我还记得他的花名。javascript

原型链设计机制一直是大多数前端开发最难理解的部分,听说当初 Brendan Eich 设计之初不想引入类的概念,可是为了将对象联系起来,加入的C++ new的概念,可是new没有办法共享属性,就在构造函数里设置了一个prototype属性,这一设计理念成为了js跟其余面向对象语言不一样的地方,同时也埋下了巨大的坑!css

为了解决由于委托机制带来的各类各样的缺点及语法问题,es6以后引入的class,class的实质仍是基于原型链封装的语法糖,可是却大大简化的前端开发的代码,也解决了不少历史遗留的问题,(这里并不想展开讨论)。可是,es6以后,原型链真的不须要被了解了吗?在知乎上有一篇被浏览了130多万的话题 :《面试一个5年的前端,却连原型链也搞不清楚,满口都是Vue,React之类的实现,这样的人该用吗?曾经引发过热议。接下来咱们就来聊聊js的原型链吧!前端

关于 new 操做符

在聊原型链以前,我想先聊聊new,这是一个常常会在面试中被问到的基础问题。怎么使用这里不详细介绍,只是提一下js里new的设计原理:java

  1. 建立一个新对象;es6

  2. 让空对象的[[prototype]](IE9如下没有该属性,在js代码里写法为__proto__)成员指向了构造函数的prototype成员对象;web

  3. 使用apply调用构造器函数,this绑定到空对象obj上;面试

  4. 返回新对象。segmentfault

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 实例的确没有必要,经过把函数定义转移到构造函数外部来解决这个问题。app

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 = {
  coolfunction() {
     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的问题

相关文章
相关标签/搜索