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

 

posted @ 2019-04-02 01:04  Lorin-Yang  阅读(1630)  评论(0编辑  收藏  举报