• es6之后,真的不需要知道原型链了吗?


     

    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的设计原理:

    1. 创建一个新对象;

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

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

    4. 返回新对象。

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

  • 相关阅读:
    MongoDB 安装及其配置
    hdu 1241 Oil Deposits
    2014ACM/ICPC亚洲区北京站
    hdu 5901 count prime & code vs 3223 素数密度
    hdu 2191 珍惜现在,感恩生活
    FOJ 2181 快来买肉松饼
    hdu 5384 Danganronpa
    hdu 2222 Keywords Search
    hdu 1300 Pearls
    2016.2.24. 《构建之法》开始阅读
  • 原文地址:https://www.cnblogs.com/ylweb/p/10640225.html
Copyright © 2020-2023  润新知