首页 > 技术文章 > 一道前端面试题引发的思考

RuMengkai 2017-03-02 10:23 原文

一、前言

昨天被朋友问道了一个关于js的题目,据他说是网上的一道面试题,我看了一下。忽然想起了以前自己参加面试时候的一些场景:

某一天收到了一个野鸡公司的面试通知,可还没有工作的我依然心花怒放,遂梳妆打扮,沐浴焚香,经过几个小时的精心准备,怀揣着一颗赤诚的心,提前两个小时来到面试地点。面试地点是一段繁华地段的CBD,走出电梯,高大上的装修瞬间让我有了种刘姥姥进大观园的感觉,公司气氛异常热闹,每个人都紧张的忙碌着,不时的嘴里喊道‘Tom啊,我的idea你think一下啊’。然后一个西装笔挺的小哥把我带到一个严肃的会议室,扔给我几张纸并用鼻孔看着我说道:‘这是笔试题,做吧’,然后头也不回的走掉。没见过世面的我紧张极了,触摸着那几张纸久久不敢打开。心中无限幻想:这么牛逼的公司,这些题不会我一道都做不上吧?不会都是英文写的吧?考的都是高大上的内容吧,我能看懂吗?挣扎中,我缓缓打开,面试题映入眼帘:

1、属性float的作用是?

2、css3新增了那些属性?

3、js中call函数有什么功能?

4、...

情景是我扯的,但是这样的事情我相信很多人都遇到过。只是想说明一些问题,有些公司似乎在提问题上面并不愿意下很多功夫,除去一些其他的原因以外,那我只能说诚意不够了,既是只是要招聘一个很初级的程序员。当然也并不是说这些题目本身有问题,深挖的话都会牵扯到许多知识。而是这些公司的做法,随便到网上搜几道题,攒到一起,就凑成了一套面试题,没有明确的考察方向,随便考一下了事,我作为应聘者对于这样的公司就一个想法:不走心啊!

毕竟技术文章,过多内容不说,单说下面这道题目,难度不大,但综合考察了很多基础的内容,个人认为不错。这里就稍作分析,过程中有什么问题,欢迎斧正。

复制代码
// 请实现下面的自定义事件 Event 对象的接口,功能见注释(测试1)
// 该 Event 对象的接口需要能被其他对象拓展复用(测试2)

// 测试1
Event.on('test', function (result) {
    console.log(result);
});
Event.on('test', function () {
    console.log('test');
});
Event.emit('test', 'hello world'); // 输出 'hello world' 和 'test'
// 测试2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
    console.log('person1');
});
person2.on('call2', function () {
    console.log('person2');
});
person1.emit('call1'); // 输出 'person1'
person1.emit('call2'); // 没有输出
person2.emit('call1'); // 没有输出
person2.emit('call2'); // 输出 'person2'
var Event = {
    // 通过on接口监听事件eventName
    // 如果事件eventName被触发,则执行callback回调函数
    on: function (eventName, callback) {
        //你的代码
    },
    // 触发事件 eventName
    emit: function (eventName) {
        //你的代码
    }
};
复制代码

 二、题目分析

这道题,分为两个部分,测试1考察的主要内容是自定义事件中的绑定与触发,测试2主要考察内容为ES6规范中Object.assign方法和对象属性方法中Object.defineProperty的应用,以及普通对象和引用对象的区别。整体发布-订阅模式的一种实现,也就是设计模式中的观察者模式。(设计模式不做过多展开,有兴趣自行搜索),让我们先来分析一下自定义事件。

2.1 自定义事件

js中的自定义事件与js事件的关系可以说是雷锋和雷峰塔的关系,不过是自定义事件拥有了一些类似事件的特性,所以,类比于js事件而成为自定义事件。自定义事件在组件开发中是一种比较常见的方式,可以有效的解决冲突与覆盖问题。

自定义事件的实质是函数。为事件对象添加对应的事件属性,属性下面对应着该属性的方法,要触发此事件时,依次调用该事件下方法即可。

自定事件的绑定是要构造一个形如下面代码的结构

Event = {
    //    事件名称
    event1 : [fn1, fn2, ...],
    event2 : [fn3, fn4, ...],
    ...
}

如果要触发某个自定义事件,实质也就是调用相应的函数即可,如调用Event对象下面的event1事件所绑定的所有方法

for( var i = 0; i < Event.event1.length; i++ ){
    Event.event1[i]();
}

来看一下js原生事件的绑定

//    事件绑定
htmlElem.addEventListener('click', function (){
    //    do something
}, false);

这段程序为DOM元素htmlElem绑定了一个click事件,触发的方式为当鼠标点击这个元素的时候即可触发。然而,自定义事件既然名为自定义,就说明是我们附加的行为,需要自己想办法来触发,点击显然不行,砸了显示器也不会触发。原生的js事件多是绑定在DOM元素,或者XMLHttpRequest等对上,属于浏览器行为事件。自定义事件的本质是函数,所以绑定并没有这样的限制。根据以上原理可编写简易的自定义事件绑定与触发方法。

复制代码
//    自定义事件的绑定
function bindEvent ( obj, eventName, fn ){
    obj.listeners = obj.listeners || {};
    obj.listeners[eventName] = obj.listeners[eventName] || [];
    obj.listeners[eventName].push( fn );
}

//    自定义事件触发
function fireEvent ( obj, eventName ){
    if( !(obj.listeners && obj.listeners[eventName]) ) return;
    for( var i = 0; i < obj.listeners[eventName].length; i++ ){
        obj.listeners[eventName][i]();
    }
}
复制代码

自定义事件绑定的方法构建了事件对象、事件名称和事件函数的一个映射关系。而事件的触发是通过调用事件对象对应事件名称属性上的方法来实现。这里只是简单说明了一下自定义事件绑定的基本原理,更多细节如事件解绑、原生事件绑定等等与此题联系不大,不做展开。

2.2 Object.assign方法

 这个方法是ES6规范中新增加的一个API,作用是将所有可枚举的属性的值从一个或多个源对象复制到目标对象。这是官方文档上的说法,用人话来说就是:合并对象。Object.assign MDN文档

用法就是:Object.assign(target, obj1, obj2, ...)

实际的效果就是依次的将obj1 obj2中的属性合并到target中,如果键相同,后面的会覆盖掉前面的。但是属性必须是自身的(不能是继承的属性)或者可枚举的(访问器属性中enumerable属性值为true),返回目标对象,也就是合并后的结果。举个栗子:

2.2.1 Object.assign的合并与覆盖的特性
复制代码
var a = {};
var b = {
    name : 'zhangsan'
}

var c = Object.assign(a, b);
console.log(c);    //    c : { name : 'zhangsan' }

var d = {
    name : 'lisi'
}
var e = Object.assign(c, d);
console.log(e);    //    e : { name : 'lisi' }

var f = {
    age : 10000
}
var g = Object.assign(e, f);
console.log(g);    //    g : { name: "lisi", age: 10000 }
复制代码
2.2.2 Object.assign 可合并自身可枚举属性

可合并的属性有两个条件:自身(非继承),可枚举(enumerable值为true)

复制代码
//    利用Object.create方法创建obj对象,obj包含三个属性,不可枚举的value2,可枚举的value3和可通过原型链找到的继承属性value1
var obj = Object.create({
    value1 : 1
}, {
    value2 : {
        value : 2
    },
    value3 : {
        value : 3,
        enumerable : true
    }
});

console.log(obj); // { value2 : 2, value3 : 3 }

var copy = Object.assign({}, obj);
console.log(copy); // { value3 : 3 }
复制代码

两次打印效果如图:(btw:就在我刚刚截图的时候才注意到,不可枚举的属性在chrome调试工具中显示的颜色同可枚举属性是有差别的,以前从没注意过。)

2.2.3 深度克隆

这里算得上这道问题的一个小坑吧。Object.assign并不能实现深度克隆,也就是说源对象如果是一个引用对象,那么它拷贝的仅仅是应用而并不是引用对象本身。

复制代码
var arr = [1, 2, 3];
var a = {
    b : arr
}

console.log(a); // a : { b : [1, 2, 3] }

var c = Object.assign({}, a);

console.log(c);    // c : { b : [1, 2, 3] }

//    向a对象中的b添加一个值
a.b.push(4);

console.log(c);    // c : { b : [1, 2, 3, 4] }
复制代码

三、解决方法

基于以上的基础知识与分析,解答此题

复制代码
var Event = {
    on : function ( eventName, fn ){
        var _this = this;
        !this.listeners && (function (){
            Object.defineProperty(_this, 'listeners', {
                value: {},
                enumerable: false
            });
        })();
        this.listeners[eventName] = this.listeners[eventName] || [];
        this.listeners[eventName].push( fn );
    },

    emit : function ( eventName ){
        if( !(this.listeners && this.listeners[eventName]) ) return;
        for( var i = 0; i < this.listeners[eventName].length; i++ ){
            this.listeners[eventName][i]( arguments[1] );
        }
    }
}
复制代码

经过测试,可以完成题目所需要求。

四、总结

此题主要涉及的技术,自定义事件、Object.assign的用法、对象数据属性设置、引用对象复制问题、arguments问题等。

总体来看是一道精心准备的题目,走心了!。其中有很多地方也没有展开来说,这里也是做一个简单的原理解释,其他内容放在其他篇幅里解释吧。

 

 

如果觉得这篇文章对你还有那么点点点点作用,点个推荐吧。

 

推荐阅读