前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我的 Web 前端面试经历——百度

我的 Web 前端面试经历——百度

作者头像
李才哥
发布2019-07-10 10:17:46
6400
发布2019-07-10 10:17:46
举报
文章被收录于专栏:李才哥
写在前面的话

博主就读于电子科技大学,大三狗一枚!

面试是个漫长的过程,从海投到收获电话面试,一面、二面、三面,一个步骤出错那么后面就宣告终结。同时,面试过程中你也可能会遇到一些面试官的刁难,甚至部分面试官会说些比较打击你的话,但是大部分面试官都是很棒的!

面试前的准备
基础知识

必须有牢固的基础知识,足够丰富的项目经历(就我而言差不多是三个完整项目经历,时间累计差不多接近一年)。

至少这上面的面试题你能全答出来,说得足够清楚!

  • web前端面试宝典1(https://github.com/markyun/My-blog/tree/master/Front-end-Developer-Questions)
  • web前端面试宝典2(https://github.com/h5bp/Front-end-Developer-Interview-Questions/tree/master/Translations/Chinese)
表述能力

表述能力,你要能把你的答案给面试官描述清楚,注意专业词汇,这将大大提高面试官对你的印象分!

简历

简历尽量一页,不要超过两页。简历内容要直奔主题,姓名、电话、邮箱、学校、项目经历!兴趣爱好之类的大部分面试官会视为垃圾信息直接过滤掉,如果你Github有什么star很多的项目千万记得贴上,这点加分非常高!如果你有自己的博客,博客上有一些含金量较高的文章的话也记得贴上自己的博客。当然最重要的一块肯定是你掌握了哪些技术,但是千万不要用网上现在比较火的进度条去表示你对技术的掌握情况,这是非常愚蠢的行为,到底什么算掌握、熟悉、精通?

简历内容,总结一下如下:

  • 姓名、电话、邮箱、学校等必要信息
  • 项目经历(注意表明使用的技术栈,自己的工作)
  • Github高星项目
  • 个人博客
  • 个人技术介绍
投递简历
  • 拉勾网
  • 实习僧
  • 企业的校招网站

就我自身而言,投递出了差不多40份简历,最后接到了7个电话面试,4个进入二面,3个进入三面,3个拿到offer!基本上进入三面以后都比较稳了,当然不排除竞争比较激烈的时候三面刷人!

关于面试是否通过与等待时间问题

首先你需要注意的一点是,电话面试如果没通过的话是肯定不会打电话通知你的。如果你电话面试通过了的话,3天之内是一般是会安排下次电话面试,直接联系你的,注意星期六星期天是不计入时间的。

面试流程
一面

最凶残,最可怕的一个环节,大部分人在这里直接被刷掉。一面会问很多基础的问题,但往往就是这些基础问题导致很多人直接被刷掉,所以打好基础尤为重要。基础问题详情请参照:

  • web前端面试宝典1(https://github.com/markyun/My-blog/tree/master/Front-end-Developer-Questions)
  • web前端面试宝典2(https://github.com/h5bp/Front-end-Developer-Interview-Questions/tree/master/Translations/Chinese)

举个例子,以下几个的异同。

代码语言:javascript
复制
line-height:15px;
line-height:150%;
line-height:1.5;
line-height:1.5em;

面试流程:

  • 自我介绍(用最简单的语言表明自己最大的优势)
  • HTML基础
  • CSS基础
  • JS基础
  • 你有什么想问的?

就我自己面试经历来看,各大公司都特别重视原生JS。同时一面中基本不会涉及到框架的问题。

二面

二面问的问题就很深入了,会针对你的项目进行深入剖析,对你简历上的技术进行深入追问,看你是否具有真才实干。

面试流程:

  • 自我介绍
  • 完整概述一个你感觉最你自己做过最棒的项目
  • 针对技术进行深入探讨
  • 你有什么想问的?
三面

能来到这一步基本上非常稳了,而且这个时候你的面试官基本上是你以后进公司的顶头上司了。

同时三面的气氛就比较轻松了,当然也会问你一些技术方面的问题。一般三面过没过自己都能根据最后面试官的口气感觉出来。

面试流程:

  • 自我介绍
  • 部分技术问题
  • 一些关于公司的介绍
  • 你有什么想问的?
HR发offer

一般三面完了,三天内会有HR联系你,询问你的一些情况,比如本科在读还是研究生在读,然后给你说一下待遇,多少钱一天啊,什么餐补,住房补助等等之类的。了解清楚后一般2天内会把offer发到你的邮箱!此刻大功告成,准备进入新公司吧!

我的一次完整面试经历——百度
一面

主要还是问web的一些基础问题,有准备的话通过还是比较容易的。

我整理了一下问题大概是这些:

  • css盒子模型
  • 页面加载如何优化
  • url->页面加载完成的整个流程
  • 优雅降级与渐进增强
  • xhtml是什么
  • ajax的优缺点
  • js组成部分
  • 解释一下变量声明提升
  • 如何跨域访问
  • js如何判断一个数组
  • 阐述一下js严格模式

还有一些其余的问题记不清了,最后面试官问我有什么问题要提的,我问了下部门的技术栈、技术沙龙之类的。最后,礼貌地说了一句:“感谢面试官百忙之中抽空来面试我,这次面试学到了很多,希望贵公司能给我一个接触前沿技术、锻炼自身的机会,谢谢面试官!”

二面

二面的面试官首先还是问了一下技术问题。

  • 负载均衡你了解吗?阐述一下
  • linux环境你熟悉吗?说一些你用过的指令
  • webpack了解吗?用过哪些功能
  • 对css预编译器有所了解吗?

还有些记不清了,大多数时间是在问项目的问题:

  • 完整概述一个你感觉最你自己做过最棒的项目

然后,面试官会根据你的回答针对性地提一下问题,举个例子:

  • 你在这个项目中用到了express,那你能说说express的特点是什么吗?
  • 有考虑过如何对项目进行优化吗?从那方面入手?

最后依然是国际惯例,我问了下部门的技术栈、技术沙龙之类的,礼貌说了下感谢的话!

三面

三面就比较轻松了,面试官会跟你了一些公司文化之类的东西,见招拆招吧,好好表现,没什么重大问题基本上就过了。

总结

面试=技术+运气+礼貌!

个人认为:

礼貌>技术>运气

一个没有礼貌的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的所有属性和对象
  • 此类可以自由拓展方法

形象点描述,就是要求可以这样:

代码语言:javascript
复制
// 假设最终的类是 MyDate,有一个getTest拓展方法
let date = new MyDate();

// 调用Date的方法,输出GMT绝对毫秒数
console.log(date.getTime());
// 调用拓展的方法,随便输出什么,譬如helloworld!
console.log(date.getTest());

于是,随手用JS中经典的组合寄生法写了一个继承,然后,刚准备完美收工,一运行,却出现了以下的情景:

但是的心情是这样的: ?囧

以前也没有遇到过类似的问题,然后自己尝试着用其它方法,多次尝试,均无果(不算暴力混合法的情况),其实回过头来看,是因为思路新奇,凭空想不到,并不是原理上有多难。。。

于是,借助强大的搜素引擎,搜集资料,最后,再自己总结了一番,才有了本文。

正文开始前,各位看官可以先暂停往下读,尝试下,在不借助任何网络资料的情况下,是否能实现上面的需求?(就以 10分钟为限吧)

分析问题的关键

借助stackoverflow上的回答。

经典的继承法有何问题

先看看本文最开始时提到的经典继承法实现,如下:

代码语言:javascript
复制
/**
 * 经典的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黑魔法

然后,再看看ES5中如何实现?

代码语言:javascript
复制
// 需要考虑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原型上方法的权利
  • 构造函数里的Date对象的 [[ptototype]](对外,浏览器中可通过 __proto__访问)指向 MyDate.prototype,然后 MyDate.prototype再指向 Date.prototype

所以最终的实例对象仍然能进行正常的原型链回溯,回溯到原本Date的所有原型方法。

这样通过一个巧妙的欺骗技巧,就实现了完美的Date继承。不过补充一点, MDN上有提到尽量不要修改对象的 [[Prototype]],因为这样可能会干涉到浏览器本身的优化。如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]

ES6大法

当然,除了上述的ES5实现,ES6中也可以直接继承(自带支持继承 Date),而且更为简单:

代码语言:javascript
复制
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写法,然后Babel打包

虽然说上述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四种继承实现分别是:(排出了混合法)

  • ES6的Class大法
  • 经典组合寄生继承法
  • 本文中的取巧做法,Date构造实例,然后更改 __proto__的那种
  • ES6的Class大法,Babel打包后的实现(无法正常调用的)
代码语言:javascript
复制
~~~~以下是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. MyDate们的proto指向不一样
  2. Object.prototype.toString.call的输出不一样
  3. 对象本质不一样,可以正常调用的 1,3都是 Date构造出的,而其它的则是 MyDate构造出的

我们上文中得出的一个结论是:由于调用的对象不是由Date构造出的实例,所以不允许调用,就算是自己的原型链上有Date.prototype也不行

但是这里有两个变量:分别是底层构造实例的方法不一样,以及对象的 Object.prototype.toString.call的输出不一样(另一个 MyDate.__proto__可以排除,因为原型链回溯肯定与它无关)。

万一它的判断是根据 Object.prototype.toString.call来的呢?那这样结论不就有误差了?

于是,根据ES6中的, Symbol.toStringTag,使用黑魔法,动态的修改下它,排除下干扰:

代码语言:javascript
复制
// 分别可以给date2,date3设置
Object.defineProperty(date2, Symbol.toStringTag, {
    get: function() {
        return "Date";
    }
});

然后在打印下看看,变成这样了:

代码语言:javascript
复制
[object Date]
[object Date]
[object Date]
[object Object]

可以看到,第二个的 MyDate2构造出的实例,虽然打印出来是 [objectDate],但是调用Date方法仍然是有错误。

此时我们可以更加准确一点的确认:由于调用的对象不是由Date构造出的实例,所以不允许调用

而且我们可以看到,就算通过黑魔法修改 Object.prototype.toString.call,内部的 [[Class]]标识位也是无法修改的。(这块知识点大概是Object.prototype.toString.call可以输出内部的[[Class]],但无法改变它,由于不是重点,这里不赘述)。

ES6继承与ES5继承的区别

从上午中的分析可以看到一点:ES6的Class写法继承是没问题的。但是换成ES5写法就不行了。

所以ES6的继承大法和ES5肯定是有区别的,那么究竟是哪里不同呢?(主要是结合的本文继承Date来说)

区别:(以 SubClassSuperClassinstance为例)

ES5中继承的实质是:(那种经典组合寄生继承法)

  • 先由子类( SubClass)构造出实例对象this
  • 然后在子类的构造函数中,将父类( SuperClass)的属性添加到 this上, SuperClass.apply(this,arguments)
  • 子类原型( SubClass.prototype)指向父类原型( SuperClass.prototype
  • 所以 instance是子类( SubClass)构造出的(所以没有父类的 [[Class]]关键标志)
  • 所以, instanceSubClassSuperClass的所有实例属性,以及可以通过原型链回溯,获取 SubClassSuperClass原型上的方法

ES6中继承的实质是:

  • 先由父类( SuperClass)构造出实例对象this,这也是为什么必须先调用父类的 super()方法(子类没有自己的this对象,需先由父类构造)
  • 然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型( SubClass.prototype),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测)
  • 然后同样,子类原型( SubClass.prototype)指向父类原型( SuperClass.prototype
  • 所以 instance是父类( SuperClass)构造出的(所以有着父类的 [[Class]]关键标志)
  • 所以, instanceSubClassSuperClass的所有实例属性,以及可以通过原型链回溯,获取 SubClassSuperClass原型上的方法

以上⬆就列举了些重要信息,其它的如静态方法的继承没有赘述。(静态方法继承实质上只需要更改下 SubClass.__proto__SuperClass即可)

可以看着这张图快速理解:

有没有发现呢:ES6中的步骤和本文中取巧继承Date的方法一模一样,不同的是ES6是语言底层的做法,有它的底层优化之处,而本文中的直接修改_proto_容易影响性能

ES6中在super中构建this的好处?

因为ES6中允许我们继承内置的类,如Date,Array,Error等。如果this先被创建出来,在传给Array等系统内置类的构造函数,这些内置类的构造函数是不认这个this的。所以需要现在super中构建出来,这样才能有着super中关键的 [[Class]]标志,才能被允许调用。(否则就算继承了,也无法调用这些内置类的方法)

构造函数与实例对象

看到这里,不知道是否对上午中频繁提到的构造函数实例对象有所混淆与困惑呢?这里稍微描述下。

要弄懂这一点,需要先知道 new一个对象到底发生了什么?先形象点说:

new MyClass()中,都做了些什么工作
代码语言:javascript
复制
function MyClass() {
    this.abc = 1;
}

MyClass.prototype.print = function() {
    console.log('this.abc:' + this.abc);
};

let instance = new MyClass();

譬如,上述就是一个标准的实例对象生成,都发生了什么呢?

步骤简述如下:(参考MDN,还有部分关于底层的描述略去-如[[Class]]标识位等)

  1. 构造函数内部,创建一个新的对象,它继承自 MyClass.prototypeletinstance=Object.create(MyClass.prototype);
  2. 使用指定的参数调用构造函数 MyClass,并将 this绑定到新创建的对象, MyClass.call(instance);,执行后拥有所有实例属性
  3. 如果构造函数返回了一个“对象”,那么这个对象会取代整个 new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象。 (一般情况下构造函数不返回任何值,不过用户如果想覆盖这个返回值,可以自己选择返回一个普通对象来覆盖。当然,返回数组也会覆盖,因为数组也是对象。)

结合上述的描述,大概可以还原成以下代码(简单还原,不考虑各种其它逻辑):

代码语言:javascript
复制
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的实例。即:

代码语言:javascript
复制
let instance = new MyClass();

此时我们就可以认为 instanceMyClass的实例,因为它的构造函数就是它。

实例就一定是由对应的构造函数构造出的么?

不一定,我们那ES5黑魔法来做示例。

代码语言:javascript
复制
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,因此可以认为 instanceMyDate的实例。

但是,实际上, instance却是由 Date构造的,我们可以继续用 ES6中的 new.target来验证。

注意⚠️:关于 new.targetMDN中的定义是:new.target返回一个指向构造方法或函数的引用

嗯哼,也就是说,返回的是构造函数。

我们可以在相应的构造中测试打印:

代码语言:javascript
复制
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之类的分析时,我们还是得明白)。

[[Class]]与Internal slot

这一部分为补充内容。

前文中一直提到一个概念:Date内部的 [[Class]]标识

其实,严格来说,不能这样泛而称之(前文中只是用这个概念是为了降低复杂度,便于理解),它可以分为以下两部分:

在ES5中,每种内置对象都定义了 [[Class]] 内部属性的值,[[Class]] 内部属性的值用于内部区分对象的种类

  • Object.prototype.toString访问的就是这个[[Class]]
  • 规范中除了通过 Object.prototype.toString,没有提供任何手段使程序访问此值。
  • 而且Object.prototype.toString输出无法被修改

而在ES5中,之前的 [[Class]] 不再使用,取而代之的是一系列的 internalslot

  • Internal slot 对应于与对象相关联并由各种ECMAScript规范算法使用的内部状态,它们没有对象属性,也不能被继承
  • 根据具体的 Internal slot 规范,这种状态可以由任何ECMAScript语言类型或特定ECMAScript规范类型值的值组成
  • 通过 Object.prototype.toString,仍然可以输出Internal slot值
  • 简单点理解(简化理解),Object.prototype.toString的流程是:如果是基本数据类型(除去Object以外的几大类型),则返回原本的slot,如果是Object类型(包括内置对象以及自己写的对象),则调用 Symbol.toStringTagSymbol.toStringTag方法的默认实现就是返回对象的Internal slot,这个方法可以被重写

这两点是有所差异的,需要区分(不过简单点可以统一理解为内置对象内部都有一个特殊标识,用来区分对应类型-不符合类型就不给调用)。

JS内置对象是这些:

代码语言:javascript
复制
"Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"

ES6新增的一些,这里未提到:(如Promise对象可以输出 [objectPromise]),而前文中提到的:

代码语言:javascript
复制
Object.defineProperty(date, Symbol.toStringTag, {
    get: function() {
        return "Date";
    }
});

它的作用是重写Symbol.toStringTag,截取date(虽然是内置对象,但是仍然属于Object)的 Object.prototype.toString的输出,让这个对象输出自己修改后的 [objectDate]

但是,仅仅是做到输出的时候变成了Date,实际上内部的 internalslot值并没有被改变,因此仍然不被认为是Date。

如何快速判断是否继承?

其实,在判断继承时,没有那么多的技巧,就只有关键的一点: [[prototype]]__ptoto__)的指向关系

譬如:

代码语言:javascript
复制
console.log(instance instanceof SubClass);
console.log(instance instanceof SuperClass);

实质上就是:

  • SubClass.prototype是否出现在 instance的原型链上
  • SuperClass.prototype是否出现在 instance的原型链上

然后,对照本文中列举的一些图,一目了然就可以看清关系。有时候,完全没有必要弄的太复杂。

写在最后的话

由于继承的介绍在网上已经多不胜数,因此本文没有再重复描述,而是由一道Date继承题引发,展开(关键就是原型链)。

不知道看到这里,各位看官是否都已经弄懂了JS中的继承呢?

另外,遇到问题时,多想一想,有时候你会发现,其实你知道的并不是那么多,然后再想一想,又会发现其实并没有这么复杂。。。

后话

博主最后去了百度某部门,想象这一个月的面试,收获颇丰,面试过程中长了不少姿势,最后感谢在前端路上遇到的每一位dalao,感谢各位的指点与帮助!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-05-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 李才哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 面试前的准备
    • 基础知识
      • 表述能力
        • 简历
          • 投递简历
            • 关于面试是否通过与等待时间问题
            • 面试流程
              • 一面
                • 二面
                  • 三面
                    • HR发offer
                    • 我的一次完整面试经历——百度
                      • 一面
                      • 二面
                        • 三面
                          • 总结
                          • 分析问题的关键
                            • 经典的继承法有何问题
                              • 为什么无法被继承?
                              • 该如何实现继承?
                                • 暴力混合法
                                  • ES5黑魔法
                                    • ES6大法
                                      • ES6写法,然后Babel打包
                                      • 几种继承的细微区别
                                      • ES6继承与ES5继承的区别
                                      • 构造函数与实例对象
                                        • new MyClass()中,都做了些什么工作
                                          • 实例就一定是由对应的构造函数构造出的么?
                                          • [[Class]]与Internal slot
                                          • 如何快速判断是否继承?
                                          • 写在最后的话
                                            • 后话
                                            相关产品与服务
                                            负载均衡
                                            负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
                                            领券
                                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档