博主就读于电子科技大学,大三狗一枚!
面试是个漫长的过程,从海投到收获电话面试,一面、二面、三面,一个步骤出错那么后面就宣告终结。同时,面试过程中你也可能会遇到一些面试官的刁难,甚至部分面试官会说些比较打击你的话,但是大部分面试官都是很棒的!
必须有牢固的基础知识,足够丰富的项目经历(就我而言差不多是三个完整项目经历,时间累计差不多接近一年)。
至少这上面的面试题你能全答出来,说得足够清楚!
表述能力,你要能把你的答案给面试官描述清楚,注意专业词汇,这将大大提高面试官对你的印象分!
简历尽量一页,不要超过两页。简历内容要直奔主题,姓名、电话、邮箱、学校、项目经历!兴趣爱好之类的大部分面试官会视为垃圾信息直接过滤掉,如果你Github有什么star很多的项目千万记得贴上,这点加分非常高!如果你有自己的博客,博客上有一些含金量较高的文章的话也记得贴上自己的博客。当然最重要的一块肯定是你掌握了哪些技术,但是千万不要用网上现在比较火的进度条去表示你对技术的掌握情况,这是非常愚蠢的行为,到底什么算掌握、熟悉、精通?
简历内容,总结一下如下:
就我自身而言,投递出了差不多40份简历,最后接到了7个电话面试,4个进入二面,3个进入三面,3个拿到offer!基本上进入三面以后都比较稳了,当然不排除竞争比较激烈的时候三面刷人!
首先你需要注意的一点是,电话面试如果没通过的话是肯定不会打电话通知你的。如果你电话面试通过了的话,3天之内是一般是会安排下次电话面试,直接联系你的,注意星期六星期天是不计入时间的。
最凶残,最可怕的一个环节,大部分人在这里直接被刷掉。一面会问很多基础的问题,但往往就是这些基础问题导致很多人直接被刷掉,所以打好基础尤为重要。基础问题详情请参照:
举个例子,以下几个的异同。
line-height:15px;
line-height:150%;
line-height:1.5;
line-height:1.5em;
面试流程:
就我自己面试经历来看,各大公司都特别重视原生JS。同时一面中基本不会涉及到框架的问题。
二面问的问题就很深入了,会针对你的项目进行深入剖析,对你简历上的技术进行深入追问,看你是否具有真才实干。
面试流程:
能来到这一步基本上非常稳了,而且这个时候你的面试官基本上是你以后进公司的顶头上司了。
同时三面的气氛就比较轻松了,当然也会问你一些技术方面的问题。一般三面过没过自己都能根据最后面试官的口气感觉出来。
面试流程:
一般三面完了,三天内会有HR联系你,询问你的一些情况,比如本科在读还是研究生在读,然后给你说一下待遇,多少钱一天啊,什么餐补,住房补助等等之类的。了解清楚后一般2天内会把offer发到你的邮箱!此刻大功告成,准备进入新公司吧!
主要还是问web的一些基础问题,有准备的话通过还是比较容易的。
我整理了一下问题大概是这些:
还有一些其余的问题记不清了,最后面试官问我有什么问题要提的,我问了下部门的技术栈、技术沙龙之类的。最后,礼貌地说了一句:“感谢面试官百忙之中抽空来面试我,这次面试学到了很多,希望贵公司能给我一个接触前沿技术、锻炼自身的机会,谢谢面试官!”
二面的面试官首先还是问了一下技术问题。
还有些记不清了,大多数时间是在问项目的问题:
然后,面试官会根据你的回答针对性地提一下问题,举个例子:
最后依然是国际惯例,我问了下部门的技术栈、技术沙龙之类的,礼貌说了下感谢的话!
三面就比较轻松了,面试官会跟你了一些公司文化之类的东西,见招拆招吧,好好表现,没什么重大问题基本上就过了。
面试=技术+运气+礼貌!
个人认为:
礼貌>技术>运气
一个没有礼貌的codder估计没面试官欢迎吧,毕竟他以后是你的同事,肯定希望是个好相处的人。总之,注重礼节,但是技术也不可缺少哦,最后运气也是有的,也许你当天遇到的面试官心情不好,刁难你也说不一定哦,但是如果能把你刁难到证明自己的技术确实有不足之处,需要加油改进哦!
下面,来看看面试题:
第一道:
在 JS 中,有 5 种基本数据类型和 1 种复杂数据类型,基本数据类型有:Undefined, Null, Boolean, Number和String;复杂数据类型是Object,Object中还细分了很多具体的类型,比如:Array, Function, Date等等。今天我们就来探讨一下,使用什么方法判断一个出一个变量的类型。
在讲解各种方法之前,我们首先定义出几个测试变量,看看后面的方法究竟能把变量的类型解析成什么样子,以下几个变量差不多包含了我们在实际编码中常用的类型。
var num = 123; var str = 'abcdef'; var bool = true; var arr = [1, 2, 3, 4]; var json = {name:'wenzi', age:25}; var func = function(){ console.log('this is function'); } var und = undefined; var nul = null; var date = new Date(); var reg = /^[a-zA-Z]{5,20}$/; var error= new Error();
1. 使用typeof检测
我们平时用的最多的就是用typeof检测变量类型了。这次,我们也使用typeof检测变量的类型:
console.log( typeof num, typeof str, typeof bool, typeof arr, typeof json, typeof func, typeof und, typeof nul, typeof date, typeof reg, typeof error ); // number string boolean object object function undefined object object object object
从输出的结果来看,arr, json, nul, date, reg, error 全部被检测为object类型,其他的变量能够被正确检测出来。当需要变量是否是number, string, boolean, function, undefined, json类型时,可以使用typeof进行判断。其他变量是判断不出类型的,包括null。
还有,typeof是区分不出array和json类型的。因为使用typeof这个变量时,array和json类型输出的都是object。
2. 使用instance检测
在 JavaScript 中,判断一个变量的类型尝尝会用 typeof 运算符,在使用 typeof 运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象,它都返回 “object”。ECMAScript 引入了另一个 Java 运算符 instanceof 来解决这个问题。instanceof 运算符与 typeof 运算符相似,用于识别正在处理的对象的类型。与 typeof 方法不同的是,instanceof 方法要求开发者明确地确认对象为某特定类型。例如:
function Person(){ } var Tom = new Person(); console.log(Tom instanceof Person); // true
我们再看看下面的例子:
function Person(){ } function Student(){ } Student.prototype = new Person(); var John = new Student(); console.log(John instanceof Student); // true console.log(John instancdof Person); // true
instanceof还能检测出多层继承的关系。
好了,我们来使用instanceof检测上面的那些变量:
console.log( num instanceof Number, str instanceof String, bool instanceof Boolean, arr instanceof Array, json instanceof Object, func instanceof Function, und instanceof Object, nul instanceof Object, date instanceof Date, reg instanceof RegExp, error instanceof Error ) // num : false // str : false // bool : false // arr : true // json : true // func : true // und : false // nul : false // date : true // reg : true // error : true
从上面的运行结果我们可以看到,num, str和bool没有检测出他的类型,但是我们使用下面的方式创建num,是可以检测出类型的:
var num = new Number(123); var str = new String('abcdef'); var boolean = new Boolean(true);
同时,我们也要看到,und和nul是检测的Object类型,才输出的true,因为js中没有Undefined和Null的这种全局类型,他们und和nul都属于Object类型,因此输出了true。
3. 使用constructor检测
在使用instanceof检测变量类型时,我们是检测不到number, 'string', bool的类型的。因此,我们需要换一种方式来解决这个问题。
constructor本来是原型对象上的属性,指向构造函数。但是根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的。
我们先来输出一下num.constructor的内容,即数字类型的变量的构造函数是什么样子的:
function Number() { [native code] }
我们可以看到它指向了Number的构造函数,因此,我们可以使用num.constructor==Number来判断num是不是Number类型的,其他的变量也类似:
function Person(){ } var Tom = new Person(); // undefined和null没有constructor属性 console.log( Tom.constructor==Person, num.constructor==Number, str.constructor==String, bool.constructor==Boolean, arr.constructor==Array, json.constructor==Object, func.constructor==Function, date.constructor==Date, reg.constructor==RegExp, error.constructor==Error ); // 所有结果均为true
从输出的结果我们可以看出,除了undefined和null,其他类型的变量均能使用constructor判断出类型。
不过使用constructor也不是保险的,因为constructor属性是可以被修改的,会导致检测出的结果不正确,例如:
function Person(){ } function Student(){ } Student.prototype = new Person(); var John = new Student(); console.log(John.constructor==Student); // false console.log(John.constructor==Person); // true
在上面的例子中,Student原型中的constructor被修改为指向到Person,导致检测不出实例对象John真实的构造函数。
同时,使用instaceof和construcor,被判断的array必须是在当前页面声明的!比如,一个页面(父页面)有一个框架,框架中引用了一个页面(子页面),在子页面中声明了一个array,并将其赋值给父页面的一个变量,这时判断该变量,Array == object.constructor;会返回false;
原因:
1、array属于引用型数据,在传递过程中,仅仅是引用地址的传递。
2、每个页面的Array原生对象所引用的地址是不一样的,在子页面声明的array,所对应的构造函数,是子页面的Array对象;父页面来进行判断,使用的Array并不等于子页面的Array;切记,不然很难跟踪问题!
4. 使用Object.prototype.toString.call
我们先不管这个是什么,先来看看他是怎么检测变量类型的:
console.log( Object.prototype.toString.call(num), Object.prototype.toString.call(str), Object.prototype.toString.call(bool), Object.prototype.toString.call(arr), Object.prototype.toString.call(json), Object.prototype.toString.call(func), Object.prototype.toString.call(und), Object.prototype.toString.call(nul), Object.prototype.toString.call(date), Object.prototype.toString.call(reg), Object.prototype.toString.call(error) ); // '[object Number]' '[object String]' '[object Boolean]' '[object Array]' '[object Object]' // '[object Function]' '[object Undefined]' '[object Null]' '[object Date]' '[object RegExp]' '[object Error]'
从输出的结果来看,Object.prototype.toString.call(变量)输出的是一个字符串,字符串里有一个数组,第一个参数是Object,第二个参数就是这个变量的类型,而且,所有变量的类型都检测出来了,我们只需要取出第二个参数即可。或者可以使用Object.prototype.toString.call(arr)=="object Array"来检测变量arr是不是数组。
我们现在再来看看ECMA里是是怎么定义Object.prototype.toString.call的:
Object.prototype.toString( ) When the toString method is called, the following steps are taken: 1. Get the [[Class]] property of this object. 2. Compute a string value by concatenating the three strings “[object “, Result (1), and “]”. 3. Return Result (2)
上面的规范定义了Object.prototype.toString的行为:首先,取得对象的一个内部属性[[Class]],然后依据这个属性,返回一个类似于”[object Array]“的字符串作为结果(看过ECMA标准的应该都知道,[[]]用来表示语言内部用到的、外部不可直接访问的属性,称为“内部属性”)。利用这个方法,再配合call,我们可以取得任何对象的内部属性[[Class]],然后把类型检测转化为字符串比较,以达到我们的目的。
5. jquery中$.type的实现
在jquery中提供了一个$.type的接口,来让我们检测变量的类型:
console.log( $.type(num), $.type(str), $.type(bool), $.type(arr), $.type(json), $.type(func), $.type(und), $.type(nul), $.type(date), $.type(reg), $.type(error) ); // number string boolean array object function undefined null date regexp error
看到输出结果,有没有一种熟悉的感觉?对,他就是上面使用Object.prototype.toString.call(变量)输出的结果的第二个参数呀。
我们这里先来对比一下上面所有方法检测出的结果,横排是使用的检测方法, 竖排是各个变量:
这样对比一下,就更能看到各个方法之间的区别了,而且Object.prototype.toString.call 和 $type 输出的结果真的很像。我们来看看jquery(2.1.2版本)内部是怎么实现$.type方法的:
// 实例对象是能直接使用原型链上的方法的 var class2type = {}; var toString = class2type.toString; // 省略部分代码... type: function( obj ) { if ( obj == null ) { return obj + ""; } // Support: Android<4.0, iOS<6 (functionish RegExp) return (typeof obj === "object" || typeof obj === "function") ? (class2type[ toString.call(obj) ] || "object") : typeof obj; }, // 省略部分代码... // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); });
我们先来看看jQuery.each的这部分:
// Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); //循环之后,`class2type`的值是: class2type = { '[object Boolean]' : 'boolean', '[object Number]' : 'number', '[object String]' : 'string', '[object Function]': 'function', '[object Array]' : 'array', '[object Date]' : 'date', '[object RegExp]' : 'regExp', '[object Object]' : 'object', '[object Error]' : 'error' }
再来看看type方法:
// type的实现 type: function( obj ) { // 若传入的是null或undefined,则直接返回这个对象的字符串 // 即若传入的对象obj是undefined,则返回"undefined" if ( obj == null ) { return obj + ""; } // Support: Android<4.0, iOS<6 (functionish RegExp) // 低版本regExp返回function类型;高版本已修正,返回object类型 // 若使用typeof检测出的obj类型是object或function,则返回class2type的值,否则返回typeof检测的类型 return (typeof obj === "object" || typeof obj === "function") ? (class2type[ toString.call(obj) ] || "object") : typeof obj; }
当typeof obj === "object" || typeof obj === "function"时,就返回class2type[ toString.call(obj)。到这儿,我们就应该明白为什么Object.prototype.toString.call和$.type那么像了吧,其实jquery中就是用Object.prototype.toString.call实现的,把'[object Boolean]'类型转成'boolean'类型并返回。若class2type存储的没有这个变量的类型,那就返回"object"。
除了"object"和"function"类型,其他的类型则使用typeof进行检测。即number, string, boolean类型的变量,使用typeof即可。
第二道
故事是从一次实际需求中开始的。。。
某天,某人向我寻求了一次帮助,要协助写一个日期工具类,要求:
Date
,拥有Date的所有属性和对象形象点描述,就是要求可以这样:
// 假设最终的类是 MyDate,有一个getTest拓展方法
let date = new MyDate();
// 调用Date的方法,输出GMT绝对毫秒数
console.log(date.getTime());
// 调用拓展的方法,随便输出什么,譬如helloworld!
console.log(date.getTest());
于是,随手用JS中经典的组合寄生法写了一个继承,然后,刚准备完美收工,一运行,却出现了以下的情景:
但是的心情是这样的: ?囧
以前也没有遇到过类似的问题,然后自己尝试着用其它方法,多次尝试,均无果(不算暴力混合法的情况),其实回过头来看,是因为思路新奇,凭空想不到,并不是原理上有多难。。。
于是,借助强大的搜素引擎,搜集资料,最后,再自己总结了一番,才有了本文。
正文开始前,各位看官可以先暂停往下读,尝试下,在不借助任何网络资料的情况下,是否能实现上面的需求?(就以 10分钟
为限吧)
借助stackoverflow上的回答。
先看看本文最开始时提到的经典继承法实现,如下:
/**
* 经典的js组合寄生继承
*/
function MyDate() {
Date.apply(this, arguments);
this.abc = 1;
}
function inherits(subClass, superClass) {
function Inner() {}
Inner.prototype = superClass.prototype;
subClass.prototype = new Inner();
subClass.prototype.constructor = subClass;
}
inherits(MyDate, Date);
MyDate.prototype.getTest = function() {
return this.getTime();
};
let date = new MyDate();
console.log(date.getTest());
就是这段代码⬆,这也是JavaScript高程(红宝书)中推荐的一种,一直用,从未失手,结果现在马失前蹄。。。
我们再回顾下它的报错:
再打印它的原型看看:
怎么看都没问题,因为按照原型链回溯规则, Date
的所有原型方法都可以通过 MyDate
对象的原型链往上回溯到。再仔细看看,发现它的关键并不是找不到方法,而是 thisisnotaDateobject.
嗯哼,也就是说,关键是:由于调用的对象不是Date的实例,所以不允许调用,就算是自己通过原型继承的也不行。
首先,看看 MDN
上的解释,上面有提到,JavaScript的日期对象只能通过 JavaScriptDate
作为构造函数来实例化。
然后再看看stackoverflow上的回答:
有提到, v8
引擎底层代码中有限制,如果调用对象的 [[Class]]
不是 Date
,则抛出错误。
总的来说,结合这两点,可以得出一个结论:要调用Date上方法的实例对象必须通过Date构造出来,否则不允许调用Date的方法。
虽然原因找到了,但是问题仍然要解决啊,真的就没办法了么?当然不是,事实上还是有不少实现的方法的。
首先,说说说下暴力的混合法,它是下面这样子的:
说到底就是:内部生成一个 Date
对象,然后此类暴露的方法中,把原有 Date
中所有的方法都代理一遍,而且严格来说,这根本算不上继承(都没有原型链回溯)。
然后,再看看ES5中如何实现?
// 需要考虑polyfill情况
Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) {
obj.__proto__ = proto;
return obj;
};
/**
* 用了点技巧的继承,实际上返回的是Date对象
*/
function MyDate() {
// bind属于Function.prototype,接收的参数是:object, param1, params2...
var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
// 更改原型指向,否则无法调用MyDate原型上的方法
// ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
Object.setPrototypeOf(dateInst, MyDate.prototype);
dateInst.abc = 1;
return dateInst;
}
// 原型重新指回Date,否则根本无法算是继承
Object.setPrototypeOf(MyDate.prototype, Date.prototype);
MyDate.prototype.getTest = function getTest() {
return this.getTime();
};
let date = new MyDate();
// 正常输出,譬如1515638988725
console.log(date.getTest());
一眼看上去不知所措?没关系,先看下图来理解:(原型链关系一目了然)
可以看到,用的是非常巧妙的一种做法:
正常继承的情况如下:
newMyDate()
返回实例对象 date
是由 MyDate
构造的date(MyDate对象)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype
这种做法的继承的情况如下:
newMyDate()
返回实例对象 date
是由 Date
构造的date(Date对象)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype
可以看出,关键点在于:
Date
对象(由 Date
构造,所以有这些内部类中的关键 [[Class]]
标志),所以它有调用 Date
原型上方法的权利[[ptototype]]
(对外,浏览器中可通过 __proto__
访问)指向 MyDate.prototype
,然后 MyDate.prototype
再指向 Date.prototype
。所以最终的实例对象仍然能进行正常的原型链回溯,回溯到原本Date的所有原型方法。
这样通过一个巧妙的欺骗技巧,就实现了完美的Date继承。不过补充一点, MDN
上有提到尽量不要修改对象的 [[Prototype]]
,因为这样可能会干涉到浏览器本身的优化。如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]
当然,除了上述的ES5实现,ES6中也可以直接继承(自带支持继承 Date
),而且更为简单:
class MyDate extends Date {
constructor() {
super();
this.abc = 1;
}
getTest() {
return this.getTime();
}
}
let date = new MyDate();
// 正常输出,譬如1515638988725
console.log(date.getTest());
对比下ES5中的实现,这个真的是简单的不行,直接使用ES6的Class语法就行了。而且,也可以正常输出。
注意:这里的正常输出环境是直接用ES6运行,不经过babel打包,打包后实质上是转化成ES5的,所以效果完全不一样。
虽然说上述ES6大法是可以直接继承Date的,但是,考虑到实质上大部分的生产环境是: ES6+Babel
直接这样用ES6 + Babel是会出问题的。
不信的话,可以自行尝试下,Babel打包成ES5后代码大致是这样的:
然后当信心满满的开始用时,会发现:
对,又出现了这个问题,也许这时候是这样的⊙?⊙
因为转译后的ES5源码中,仍然是通过 MyDate
来构造,而 MyDate
的构造中又无法修改属于 Date
内部的 [[Class]]
之类的私有标志,因此构造出的对象仍然不允许调用 Date
方法(调用时,被引擎底层代码识别为 [[Class]]
标志不符合,不允许调用,抛出错误)。
由此可见,ES6继承的内部实现和Babel打包编译出来的实现是有区别的。(虽说Babel的polyfill一般会按照定义的规范去实现的,但也不要过度迷信)。
虽然上述提到的三种方法都可以达到继承 Date
的目的-混合法严格说不能算继承,只不过是另类实现。
于是,将所有能打印的主要信息都打印出来,分析几种继承的区别,大致场景是这样的:
可以参考:( 请进入调试模式)https://dailc.github.io/fe-interview/demo/extends_date.html
从上往下, 1,2,3,4
四种继承实现分别是:(排出了混合法)
__proto__
的那种~~~~以下是MyDate们的prototype~~~~~~~~~
Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ}
Date {getTest: ƒ, constructor: ƒ}
Date {constructor: ƒ, getTest: ƒ}
~~~~以下是new出的对象~~~~~~~~~
Sat Jan 13 2018 21:58:55 GMT+0800 (CST)
MyDate2 {abc: 1}
Sat Jan 13 2018 21:58:55 GMT+0800 (CST)
MyDate {abc: 1}
~~~~以下是new出的对象的Object.prototype.toString.call~~~~~~~~~
[object Date]
[object Object]
[object Date]
[object Object]
~~~~以下是MyDate们的__proto__~~~~~~~~~
ƒ Date() { [native code] }
ƒ () { [native code] }
ƒ () { [native code] }
ƒ Date() { [native code] }
~~~~以下是new出的对象的__proto__~~~~~~~~~
Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ}
Date {getTest: ƒ, constructor: ƒ}
Date {constructor: ƒ, getTest: ƒ}
~~~~以下是对象的__proto__与MyDate们的prototype比较~~~~~~~~~
true
true
true
true
看出,主要差别有几点:
1,3
都是 Date
构造出的,而其它的则是 MyDate
构造出的我们上文中得出的一个结论是:由于调用的对象不是由Date构造出的实例,所以不允许调用,就算是自己的原型链上有Date.prototype也不行
但是这里有两个变量:分别是底层构造实例的方法不一样,以及对象的 Object.prototype.toString.call
的输出不一样(另一个 MyDate.__proto__
可以排除,因为原型链回溯肯定与它无关)。
万一它的判断是根据 Object.prototype.toString.call
来的呢?那这样结论不就有误差了?
于是,根据ES6中的, Symbol.toStringTag
,使用黑魔法,动态的修改下它,排除下干扰:
// 分别可以给date2,date3设置
Object.defineProperty(date2, Symbol.toStringTag, {
get: function() {
return "Date";
}
});
然后在打印下看看,变成这样了:
[object Date]
[object Date]
[object Date]
[object Object]
可以看到,第二个的 MyDate2
构造出的实例,虽然打印出来是 [objectDate]
,但是调用Date方法仍然是有错误。
此时我们可以更加准确一点的确认:由于调用的对象不是由Date构造出的实例,所以不允许调用。
而且我们可以看到,就算通过黑魔法修改 Object.prototype.toString.call
,内部的 [[Class]]
标识位也是无法修改的。(这块知识点大概是Object.prototype.toString.call可以输出内部的[[Class]],但无法改变它,由于不是重点,这里不赘述)。
从上午中的分析可以看到一点:ES6的Class写法继承是没问题的。但是换成ES5写法就不行了。
所以ES6的继承大法和ES5肯定是有区别的,那么究竟是哪里不同呢?(主要是结合的本文继承Date来说)
区别:(以 SubClass
, SuperClass
, instance
为例)
ES5中继承的实质是:(那种经典组合寄生继承法)
SubClass
)构造出实例对象thisSuperClass
)的属性添加到 this
上, SuperClass.apply(this,arguments)
SubClass.prototype
)指向父类原型( SuperClass.prototype
)instance
是子类( SubClass
)构造出的(所以没有父类的 [[Class]]
关键标志)instance
有 SubClass
和 SuperClass
的所有实例属性,以及可以通过原型链回溯,获取 SubClass
和 SuperClass
原型上的方法ES6中继承的实质是:
SuperClass
)构造出实例对象this,这也是为什么必须先调用父类的 super()
方法(子类没有自己的this对象,需先由父类构造)SubClass.prototype
),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测)SubClass.prototype
)指向父类原型( SuperClass.prototype
)instance
是父类( SuperClass
)构造出的(所以有着父类的 [[Class]]
关键标志)instance
有 SubClass
和 SuperClass
的所有实例属性,以及可以通过原型链回溯,获取 SubClass
和 SuperClass
原型上的方法以上⬆就列举了些重要信息,其它的如静态方法的继承没有赘述。(静态方法继承实质上只需要更改下 SubClass.__proto__
到 SuperClass
即可)
可以看着这张图快速理解:
有没有发现呢:ES6中的步骤和本文中取巧继承Date的方法一模一样,不同的是ES6是语言底层的做法,有它的底层优化之处,而本文中的直接修改_proto_容易影响性能。
ES6中在super中构建this的好处?
因为ES6中允许我们继承内置的类,如Date,Array,Error等。如果this先被创建出来,在传给Array等系统内置类的构造函数,这些内置类的构造函数是不认这个this的。所以需要现在super中构建出来,这样才能有着super中关键的 [[Class]]
标志,才能被允许调用。(否则就算继承了,也无法调用这些内置类的方法)
看到这里,不知道是否对上午中频繁提到的构造函数,实例对象有所混淆与困惑呢?这里稍微描述下。
要弄懂这一点,需要先知道 new
一个对象到底发生了什么?先形象点说:
function MyClass() {
this.abc = 1;
}
MyClass.prototype.print = function() {
console.log('this.abc:' + this.abc);
};
let instance = new MyClass();
譬如,上述就是一个标准的实例对象生成,都发生了什么呢?
步骤简述如下:(参考MDN,还有部分关于底层的描述略去-如[[Class]]标识位等)
MyClass.prototype
, letinstance=Object.create(MyClass.prototype);
MyClass
,并将 this绑定到新创建的对象, MyClass.call(instance);
,执行后拥有所有实例属性new
出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象。 (一般情况下构造函数不返回任何值,不过用户如果想覆盖这个返回值,可以自己选择返回一个普通对象来覆盖。当然,返回数组也会覆盖,因为数组也是对象。)结合上述的描述,大概可以还原成以下代码(简单还原,不考虑各种其它逻辑):
let instance = Object.create(MyClass.prototype);
let innerConstructReturn = MyClass.call(instance);
let innerConstructReturnIsObj = typeof innerConstructReturn === 'object' || typeof innerConstructReturn === 'function';
return innerConstructReturnIsObj ? innerConstructReturn : instance;
注意⚠️:普通的函数构建,可以简单的认为就是上述步骤。实际上对于一些内置类(如Date等),并没有这么简单,还有一些自己的隐藏逻辑,譬如 [[Class]]
标识位等一些重要私有属性。譬如可以在MDN上看到,以常规函数调用Date(即不加 new 操作符)将会返回一个字符串,而不是一个日期对象,如果这样模拟的话会无效。
觉得看起来比较繁琐?可以看下图梳理:
那现在再回头看看。
什么是构造函数?
如上述中的 MyClass
就是一个构造函数,在内部它构造出了 instance
对象。
什么是实例对象?
instance
就是一个实例对象,它是通过 new
出来的?
实例与构造的关系
有时候浅显点,可以认为构造函数是xxx就是xxx的实例。即:
let instance = new MyClass();
此时我们就可以认为 instance
是 MyClass
的实例,因为它的构造函数就是它。
不一定,我们那ES5黑魔法来做示例。
function MyDate() {
// bind属于Function.prototype,接收的参数是:object, param1, params2...
var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
// 更改原型指向,否则无法调用MyDate原型上的方法
// ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
Object.setPrototypeOf(dateInst, MyDate.prototype);
dateInst.abc = 1;
return dateInst;
}
我们可以看到 instance
的最终指向的原型是 MyDate.prototype
,而 MyDate.prototype
的构造函数是 MyDate
,因此可以认为 instance
是 MyDate
的实例。
但是,实际上, instance
却是由 Date
构造的,我们可以继续用 ES6
中的 new.target
来验证。
注意⚠️:关于 new.target
, MDN
中的定义是:new.target返回一个指向构造方法或函数的引用。
嗯哼,也就是说,返回的是构造函数。
我们可以在相应的构造中测试打印:
class MyDate extends Date {
constructor() {
super();
this.abc = 1;
console.log('~~~new.target.name:MyDate~~~~');
console.log(new.target.name);
}
}
// new操作时的打印结果是:
// ~~~new.target.name:MyDate~~~~
// MyDate
然后,可以在上面的示例中看到,就算是ES6的Class继承, MyDate
构造中打印 new.target
也显示 MyDate
,但实际上它是由 Date
来构造(有着 Date
关键的 [[Class]]
标志,因为如果不是Date构造(如没有标志)是无法调用Date的方法的)。
这也算是一次小小的勘误吧。
所以,实际上用 new.target
是无法判断实例对象到底是由哪一个构造构造的(这里指的是判断底层真正的 [[Class]]
标志来源的构造)。
再回到结论:实例对象不一定就是由它的原型上的构造函数构造的,有可能构造函数内部有着寄生等逻辑,偷偷的用另一个函数来构造了下,当然,简单情况下,我们直接说实例对象由对应构造函数构造也没错(不过,在涉及到这种Date之类的分析时,我们还是得明白)。
这一部分为补充内容。
前文中一直提到一个概念:Date内部的 [[Class]]
标识。
其实,严格来说,不能这样泛而称之(前文中只是用这个概念是为了降低复杂度,便于理解),它可以分为以下两部分:
在ES5中,每种内置对象都定义了 [[Class]] 内部属性的值,[[Class]] 内部属性的值用于内部区分对象的种类
Object.prototype.toString
访问的就是这个[[Class]]Object.prototype.toString
,没有提供任何手段使程序访问此值。而在ES5中,之前的 [[Class]] 不再使用,取而代之的是一系列的 internalslot
Object.prototype.toString
,仍然可以输出Internal slot值Symbol.toStringTag
。 Symbol.toStringTag
方法的默认实现就是返回对象的Internal slot,这个方法可以被重写这两点是有所差异的,需要区分(不过简单点可以统一理解为内置对象内部都有一个特殊标识,用来区分对应类型-不符合类型就不给调用)。
JS内置对象是这些:
"Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"
ES6新增的一些,这里未提到:(如Promise对象可以输出 [objectPromise]
),而前文中提到的:
Object.defineProperty(date, Symbol.toStringTag, {
get: function() {
return "Date";
}
});
它的作用是重写Symbol.toStringTag,截取date(虽然是内置对象,但是仍然属于Object)的 Object.prototype.toString
的输出,让这个对象输出自己修改后的 [objectDate]
。
但是,仅仅是做到输出的时候变成了Date,实际上内部的 internalslot
值并没有被改变,因此仍然不被认为是Date。
其实,在判断继承时,没有那么多的技巧,就只有关键的一点: [[prototype]]
( __ptoto__
)的指向关系。
譬如:
console.log(instance instanceof SubClass);
console.log(instance instanceof SuperClass);
实质上就是:
SubClass.prototype
是否出现在 instance
的原型链上SuperClass.prototype
是否出现在 instance
的原型链上然后,对照本文中列举的一些图,一目了然就可以看清关系。有时候,完全没有必要弄的太复杂。
由于继承的介绍在网上已经多不胜数,因此本文没有再重复描述,而是由一道Date继承题引发,展开(关键就是原型链)。
不知道看到这里,各位看官是否都已经弄懂了JS中的继承呢?
另外,遇到问题时,多想一想,有时候你会发现,其实你知道的并不是那么多,然后再想一想,又会发现其实并没有这么复杂。。。
博主最后去了百度某部门,想象这一个月的面试,收获颇丰,面试过程中长了不少姿势,最后感谢在前端路上遇到的每一位dalao,感谢各位的指点与帮助!