听说你精通原生JavaScript,来试试这份题目吧

982 阅读9分钟

前言

一年经验侥幸拿到字节跳动Offer,最近面了那么4,5家,发现无论大公司还是小公司,都比较注重原生Javascript,而大点的公司框架源码更是必问。所以我重新看了遍MDN以及其他的书籍,自己总结出了如下的题目,目的是提升对底层原生知识的了解,其实我自己基础也不是特别扎实,反正写出来大家可以一起讨论~

题目

  • 请写一个函数JudgePropsOnlyInPrototype(obj, key)判断key这个属性是否仅仅存在于obj对象的原型上
function JudgePropsOnlyInPrototype(obj, key){
    var hasOwn = Object.hasOwnProperty;
    return (key in obj) && !hasOwn.call(obj,key)
}

解释: returnin运算符判定了key是否在对象本身或者其原型上,而后面的!hasOwn则判定了key不在对象本身上,则2者相结合就能够判定出key是否只在原型上,注意这里用了call而不是直接obj.hasOwnProperty(key),这是因为某些对象根本没有hasOwnProperty这个方法,比如Object.create(null),这个对象完全纯净和空白,没有任何原型上的方法和属性,所以这样写健壮性高一些


  • 请解释Js中静态方法,实例方法,原型方法的区别和使用场景

静态方法:

function Animal(type){
    this.type = type
}
Animal.staticMethod = function(){
    console.log("I'm a static method!")
}
Animal.staticMethod()

简而言之,静态方法(static method)就是定义在构造函数上或者类本身上的方法,这类方法无法通过类的实例来访问,在方法内部无法通过this来访问类内部的变量,那么你可能会问什么样的方法要被定义为静态方法,那就是不需要访问类属性的方法都可以写成静态的,这类方法一般都是 Helper 方法,即对输入进行处理再得到一个输出,与对象的成员无关,比如Math.abs这种就是静态方法,继续深入思考为什么要有静态方法,原因我觉得就是节约内存使用量,因为静态方法可以所有实例公用,不用再放到每一个实例或者其原型上

实例方法:

function Animal(type){
    this.type = type
    this.run = function(){
        console.log('run')
    }
}
var animal = new Animal()
animal.run()

上面的run就是实例方法,实例方法就是一个类new出来的实例上的方法,这个方法存在于实例对象本身上,在构造函数中通过this定义,一般来说定义一个方法为实例方法是要求这个方法是该实例独有的,不和其他实例共享,否则就应该定义为原型方法

原型方法:

function Animal(type){
    this.type = type
}
Animal.prototype.run = function(){
    console.log('run')
}
var animal = new Animal()
animal.run()

这个run就是原型方法,通过prototype属性定义,这个方法不存在于new出来的实例上,只存在于其原型上,实例可以通过原型链访问原型方法,原型方法的好处在于节约内存,把多个实例公用的方法提取出来放到prototype中去,这也就是为什么要定义原型这个概念

所以可以再对比下静态方法和原型方法的区别加深理解

  • 请写一个函数实现instanceof的功能
function instanceOf(obj,Cons){
    var protoType = Cons.prototype
    var proto = obj.__proto__
    while(proto !== null){
        if(proto === protoType){
            return true
        }
        proto = proto.__proto__
    }
    return false
}

这道题好几个公司都问了,提问方式是你是否知道instanceof的原理,这道题很重要,因为它包含了原型和原型链的知识,面试官不会主动说明这是原型或原型链。首先instanceof是一个关键词,用法a instanceof A,左操作数是一个对象,右操作数是一个构造函数,意思就是判断a是否是A的实例,那么怎么判断呢,就是通过在左操作数a的原型链上一步一步往上早,然后依次比对a的__proto__和右操作数的prototype是否相等,如果相等则a是A的实例,直到搜索原型链末端null,此时返回false。所以这道题就是考察原型和原型链的知识点,另外最好用Object.getPrototypeOf获取原型,而不是用__proto__

  • 请写一个函数function getLongestPath(root),该函数的参数是html节点,返回值是dom树从根节点开始到叶节点的最长路径,用数组表示,数组中每一个元素是dom节点的标签名
function getLongestPath(root){
    if(!root) return [];
    var tempList = [];
    for(var i=0;i<root.children.length;i++){
    	var ret = getLongestPath(root.children[i]);
    	if(ret.length>tempList.length){
    	    tempList = ret.slice();
    	}
    }
    tempList.push(root.nodeName.toLowerCase());
    return temoList
}

这就是一个递归求多叉树深度的问题,leetcode上有求二叉树深度的问题,这个就是一个变种,且要求出最长路径,原理是一样的,用一个数组保存每个节点到其叶节点的最长路径,然后for循环遍历其所有子节点,不断更新最大长度,注意不要用childeNodes而是要用children,因为前者包含文本和注释节点,不在考虑范围内,另外最好用nodeName代替tagName,因为tagName只适用于元素节点

  • 请解释什么是类数组,并举例说明
var arrayLikeObj = {
    '1':'a',
    '2':'b',
    '3':'c',
    length:3
}

这就是类数组,首先它是一个对象而不是数组,然后拥有length属性,且其他的key都是非负整数的字符串,熟知的arguments对象就是类数组,然后document.getElementsByTagName返回值也是类数组

类数组转化为数组可以用Array.from或者Array.prototype.slice.call

  • 请在js中实现函数重载
function overload(){
    var args = arguments
    var strategy = {
        '0':function methodA(){},
        '1':function methodB(){},
        '2':function methodC(){}
    }
    return strategy[args.length](...args)
}

其实就是一个策略模式的应用,通过判断arguments这个对象的长度,也就是参数的个数,然后根据不同个数执行对应的方法而已

  • 请在数组的原型上实现push方法
Array.prototype.push = function(){
    for(var i=0;i<arguments.length;i++){
        this[this.length] = arguments[i]
    }
}

这里注意push可以有多个参数,所以要for循环遍历arguments对象,然后this就是调用push时的数组,直接给length那个位置赋值即可扩充数组,注意length会自增,不用再写this.length++

  • 请用2种方式实现一个函数Log,该函数在执行奇数次时打印1,偶数次时打印2,不能用全局变量
    也就是要达到如下目的
function Log(){...}
Log() //1
Log() //2
Log() //1
Log() //2

答案就是使用闭包和静态属性而已,先说闭包,首先题目中说不能使用全局变量,第一反应就是闭包,如下

var Log = (function(){
    var cnt = 1
    return function(){
        if(cnt === 1){
            console.log(1)
            cnt++
        }else{
            console.log(2)
            cnt--
        }
    }
})()

这就是闭包加自执行函数,Log函数就是里面return的那个function,这个返回的函数引用这外部函数的cnt变量,因此构成闭包结构,cnt没有被回收且cnt是局部变量,下面说下静态方法实现

function Log(){
    if(Log.cnt === 1){
        console.log(1)
        cnt++
    }else{
        console.log2)
        cnt--
    }
}
Log.cnt = 1;

这就是static属性,这个cnt属性是加在Log本身上的,不是全局变量,且可以在Log内部访问到,es6的class要使用static关键字定义静态方法

  • 请解释Object.create的作用并模拟实现一个该方法 这个方法就是用来创建对象的,函数签名如下
Object.create(proto, [propertiesObject])

第一个参数是原型对象,第二个参数是一个对象,对象的key是要添加到返回值对象的属性,value是descriptor对象,用法如下

var proto = {a:1}
var o = Object.create(proto,{p: {
  value: 42,
  writable: true,
  enumerable: true,
  configurable: true
} })
console.log(o)

第一个参数proto是生成的对象的原型对象,即o.__proto__ === proto,然后第二个参数的属性都加在o对象本身上而不是原型上,这个方法可以实现继承,如下

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

下面模拟实现Object.create

function ObjCreate(proto){
    var f = function(){}
    f.prototype = proto
    return new f()
}

其实很简单,就是显示指明f的原型为proto并返回f的实例对象

  • 最后一题,也是很容易忽略的一点,请说明下面的继承方法有什么问题
function Parent() {};
function Son() {}
Son.prototype = Object.create(Parent.prototype);

有2个问题,第一个是这仅仅是基于原型链的继承,Parent构造函数内的东西没有继承下来,需要在Son的构造函数内添加Parent.call(this),第二点在于没有写如下的代码

Son.prototype.constructor = Son

这有可能导致问题出现,考虑如下代码

function Parent() {};
function CreatedConstructor() {}

CreatedConstructor.prototype = Object.create(Parent.prototype);

CreatedConstructor.prototype.create = function create() {
  return new this.constructor();
}

new CreatedConstructor().create().create(); // error undefined is not a function since constructor === Parent

这里的CreatedConstructor的原型上有一个create方法,先看return new this.construtor()这句话,这种用法非常少见,首先要了解constructor是啥,constructor就是一个属性,在构造函数的原型上存在,且指向构造函数本身,constructor就是一个构造函数,所以可以像这么来调用constructor(),然后它必须要通过this来访问,最后new操作符说明生成了一个对象,这个对象就是CreatedConstructor的实例,也就是说create方法创建了实例本身,就和new CreateConstrutor()的返回值是一样的

好,上面的问题在于通过CreatedConstructor.prototype = Object.create(Parent.prototype)这句话进行原型继承后,CreateConstructor这个构造函数的原型的construtor已经被改写为Parent的constructor,那么我们来看最后一句话

new CreatedConstructor().create().create()

先看第一个create,这就是new了一个对象并调用其create方法,由于create方法返回实例本身,因此它上面理应还有create方法可以调用,但是此时报错,为什么,就是因为CreateConstructor这个构造函数的原型的construtor已经被改写为Parent的constructor,所以返回的是Parent的实例,Parent原型上根本没有create方法,所以报错。因此我们需要加上如下代码就能够保证CreateConstructor的原型上的构造函数是CreateConstructor本身

CreateConstructor.prototype.constructor = CreateConstructor

像上面这种题目如果出在面试中应该大部分人都答不出来吧,毕竟比较细节