前端面试准备

197 阅读18分钟

记录近期面试题目,以及论坛里其余技术大佬的面试题梳理,缓慢更新中。。。

HTML

H5标签语义化

  • H5新增标签:<header> <footer> <article> <canvas> <section> <nav> <video>
  • 语义化优点:便于阅读和维护,便于对浏览器、搜索引擎解析

WebStorage、cookie、session区别

  1. session是服务器端用于存储数据,存储特定用户会话所需的属性和配置信息。
  2. cookie、sessionStorage、localStorage是用于浏览器端存储数据。
  3. cookie数据不能超过4k,适用于会话标识,设置了cookie后,数据会发送到服务端,cookie在设置了cookie过期时间之前一直有效,即使关闭窗口或者浏览器。
  4. webStorage数据存储可达到5m,且数据保存在本地不会发送到服务端。localStorage数据存储永久有效,sessionStorage仅在关闭浏览器之前有效。
特性 Cookie localStorage sessionStorage
数据的生命期 一般由服务器生成,可设置失效时间。如果在浏览器生成,默认是关闭浏览器后失效 除非被清除,否则永久保存 仅在当前会话下有效,关闭页面或浏览器后被清楚
存放数据大小 4k左右 一般为5MB
与服务器端通信 每次都会携带在HTTP头中,如果使用Cookie保存过多数据会带来性能问题 仅在浏览器中保存,不参与和服务器的通信
localStorage.getItem('name');
localStorage.setItem('name','hk');
localStorage.removeItem('name');

HTML自定义属性data-*

<div id="test" ></div>

读写方式:

  • 直接在元素标签书写:
<div id="test" data-age="24"></div>`
  • js操作:
let str=document.getElementById("test");
str.dataset.age="24";

CSS

行内元素、块级元素、空元素

  • 行内元素:宽度、高度由内容决定,与其他元素共占一行的元素。例如<input> <i><span> <a>
  • 块级元素:默认宽度由父容器决定,默认高度由内容决定,独占一行并可以设置宽高的元素。例如<p> <h1>~<h6> <div> <ul> <table>
  • 可通过设置display:inline-block,打破其壁垒。

display可选值:inlineblockinline-blockinline是将对象内联展示,即都在同一行内,不能设置其宽高;block是将对象作为块级元素展示,另起一行;inline-block是将对象呈递为内联块级,既在同一行内,也能设置其宽高。

  • display:inline-block时元素间会因为标签段之间的空格会产生间隙,消除方法有:
    1. 设置元素margin为负值
    2. 设置父容器font-size为0,元素font-size为1rem

CSS reset

重置CSS样式,尽可能使网页在各个浏览器中相差不大,在网上找reset.css文件引用即可。

CSS盒模型

box-sizing属性可设置为border-boxcontent-box

  • border-box中,整个div的宽、高包括padding、border
  • content-box中,div的宽、高则不包括以上元素。

CSS单位

单位 描述
% 百分比
px 像素,绝对单位,计算机屏幕一个点为1px
em 相对单位,相对父元素运算。如果父元素字体大小为14px,子元素为2em,则子元素字体大小为28px
rem 相对单位,相对根元素html运算。如果html元素字体大小为14px,子元素为2rem,则子元素字体大小为28px

CSS选择器及其优先级

  1. 通配符:*
  2. ID选择器:#id
  3. 类选择器:.class
  4. 元素选择器:p div
  5. 后代选择器:p span
  6. 伪类选择器:a:hover
  7. 属性选择器:input[type="text"]
  8. 子元素选择器:li:first-child

!important>行内样式>#id>.class>元素和伪元素>*>继承>默认

CSS常见布局

  1. 水平垂直居中

    • Flex实现
    <div class="container">
    <div class="center"></div>
    </div>
    
    .tcontainer{
        width: 900px;
        height: 300px;
        margin: 0 auto;
        background-color: gray;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    .tcenter{
        width: 100px;
        height: 100px;
        background-color: #42b983;
    }
    
    • position:absolute实现
    .container{
        width: 900px;
        height: 300px;
        margin: 0 auto;
        background-color: gray;
        position: relative;
      }
      .center{
        width: 100px;
        height: 100px;
        background-color: #42b983;
        position: absolute;
        top: 50%;
        left: 50%;
        /* 下面两种方式均可 */
        /*margin-left: -50px;*/
        /*margin-top: -50px;*/
        transform: translate(-50%,-50%);
      }
    
    • 水平居中
      • 行内元素:display: inline-block; text-align: center;
      • 块级元素:margin: 0 auto
      • Flex:display: flex; justify-content: center;
    • 垂直居中
      • Flex:display: flex; align-items: center;
  2. 两列布局,一侧固定,另一侧自适应

    • Flex布局
    <div class="container">
    <div class="left"></div>
    <div class="right"></div>
    </div>
    
    .container {
        display: flex;
    }
    .left {
        width: 100px;
        height: 300px;
        background-color: aliceblue;
    }
    .right {
        width: 100%;
        height: 300px;
        background-color: pink;
    }
    
    • float布局
    .left {
        width: 100px;
        height: 300px;
        background-color: aliceblue;
        float: left;
    }  
    .right {
        margin-left: 100px;
        height: 300px;
        background-color: pink;
    }
    
  3. 三列布局 可参考两列布局

BFC

  • BFC:块级格式化上下文,是一个独立的渲染区域,使得BFC内部元素和外部元素定位不会互相影响。

  • 作用:防止margin重叠,清除内部浮动

  • 产生规则:display:inline-block;position:absolute/fixed

  • 拓展:HTML有浮动流、定位流、文档流。文档流即正常HTML的渲染规则,块级元素新开一行、内联元素公用一行。浮动流和定位流能打破文档流,使得元素原本所在位置被清除,脱离文档流。设置了float,或者positionabsolutefixed时,会脱离文档流。

CSS3新特性

  • transition 过渡
  • transform 旋转、缩放、移动或者倾斜
  • animation 动画
  • gradient 渐变
  • shadow 阴影
  • border-radius 圆角
  • @Keyframes规则,指定从目前样式更改为新的样式
<div id="red"></div>
  #red {
    width: 50px;
    height: 50px;
    animation: mymove 2s;
    position: relative;
    /*以下非必须 */
    border-radius: 50%;
    box-shadow: 10px 10px 5px #888888;
    background: linear-gradient(deeppink,black);
  }
  @keyframes mymove {
    from {
      background-color: deeppink;
      left: 0px;
    }
    to {
      background-color: green;
      left: 200px;
    }
  }

Less

CSS预编译器,添加变量、嵌套规则、混合、函数等技术,使CSS更具维护性、扩展性。

网站页面适配hack

待更新

画三角形

#div1{
    width: 0;
    height: 0;
    border: 5px solid transparent;
    border-bottom: 5px solid red;
  }

浏览器如何解析CSS选择器

.mod p span{ color:red}

浏览器从右向左解析,先找到html中所有class='red'span元素,找到后再查找其父元素中是否有p元素,再判断p元素的父元素中是否包含mod类,如果都存在则匹配上。

JavaScript

js数据类型

  • 基本数据类型(NullNumberStringBooleanUndefinedSymbol)和引用数据类型(Object)。
  • 基本数据类型属于值类型,存放在栈内存中,==时比较值,===时比较数据类型和值。
  • 引用数据类型可以细分为ObjectArrayFunctionDate等,存放在堆内存中,比较时是引用的比较(即其地址)。
  • 类型判断typeofinstanceof
var a = {};
typeof a; //Object
a instanceof Object; //true
  • typeof会把Null类型和Object都返回Object类型,并不能细分出ArrayFunction。判断是否为数组方法:
var array = [];
console.log(array instanceof Array);
console.log(Array.isArray(array));
console.log(array.constructor === Array);
console.log(Object.prototype.toString.apply(array) === "[object Array]");

比较运算符

  • 严格比较运算符===和转换比较运算符==。对于严格比较运算符,会比较操作数类型和值,对于转换比较运算符,会在进行比较前,转换两个操作数为相同的类型。
  • NAN不和任何值相等,包括自身,正数0和负数0恒等。
  • 对于nullundefined而言,使用严格运算符比较自身,转换运算符进行互相比较。
var b, a = null;
console.log(a == b);//true
console.log(a === b);//false

var c, d = NaN;
console.log(c == d);//false
console.log(c === d);//false

console.log(+0 == -0);//true
console.log(+0 === -0);//true

var e, f;
console.log(e == f);//true
console.log(e === f);//true

ajax的步骤

    //第一步,创建XMLHttpRequest对象
      var xhttp;
      if (window.XMLHttpRequest) {
        xhttp = new XMLHttpRequest();
      } else {
        xhttp = new ActiveXObject("Microsoft.XMLHTTP");
      }
      //第二步,配置请求信息
      xhttp.open("get", "", true);
      //post请求时,必须配置请求头信息
      //xhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
      //第三步,发送请求(post的参数在此处配置传递)
      xhttp.send();
      //第四步,创建回调函数
      xhttp.onreadystatechange = function () {
        if (xhttp.readyState === 4 && xhttp.status === 200) {

        }
      }

get方式传参,参数的提取方法

getParams: function () {
      let url = "http://index?a=1&b=2&c";
      url = url.substr(url.indexOf("?"));
      let result = {};
      if (url.includes("?")) {
        let str = url.substr(1);
        str = str.split("&");
        for (let i = 0; i < str.length; i++){
          result[str[i].split("=")[0]] = str[i].split("=")[1];
        }
      }
      return result;
    }

原型与原型链

  1. 原型链

    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.eat = function (name) {
        console.log("eating");
      }
    }
    
    Person.prototype.dance = function (name) {
      console.log(name + "dancing");
    }
    
    var p1 = new Person("hk", "26");
    var p2 = new Person("tdf", "25");
    
    console.log(p1.__proto__ === Person.prototype); //true
    console.log(Person.prototype.__proto__ === Object.prototype); //true
    console.log(Object.prototype.__proto__ === null); //true
    
    console.log(p1.constructor === Person); //true
    console.log(Object.getPrototypeOf(p1) === p1.__proto__); //true
    
    • 每个实例对象(p1p2)都有一个私有属性(__proto__)指向它的原型对象(prototype)。该原型对象也能看做是实例对象,通过其私有属性(__proto__)指向它的原型对象,层层向上直到一个对象的原型对象为null。根据定义,null没有原型,并作为原型链中的最后一个环节。
    • 实例对象p1,构造函数Person,原型对象Person.prototype之间关系为:
    //仅实例具有__proto__属性,仅构造函数具有prototype属性
    p1.__proto__ === Person.prototype;
    Person.prototype.__proto__ === Object.prototype;
    Object.prototype.__proto__ === null;
    p1.constructor === Person;
    //由上得出寻找p1的原型过程还能写为:
    p1.__proto__.__proto__.__proto__ === null;
    //从ES6开始,__proto__属性可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问
    Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(p1))) === null
    //p1的原型链为:
    //p1-->Person.prototype-->Object.prototype-->null
    
  2. new运算符作用

    • 在JavaScript中,创建对象和生成原型链的方法包括:词法结构创建对象、构造器创建对象、Object.create创建对象、class关键字创建对象。
    • 词法结构创建对象
    var o = {a: 1};
    //原型链:
    //o-->Object.prototype-->null
    var a = [1, 2, 3];
    //原型链:
    //a-->Array.prototype-->Object.prototype-->null
    
    • 构造器创建对象(new运算符)

    在 JavaScript 中,构造器其实就是一个普通的函数。当使用new操作符来作用这个函数时,它就可以被称为构造方法(构造函数)。

    var p1 = new Person("hk", "26");
    //实际执行内容如下:
    //创建一个空对象
    var p1 = new Object();
    //将空对象的原型指向Person.prototype
    p1.__proto__ = Person.prototype;
    //将构造函数Person的this指向新对象p1,执行构造函数并返回新对象
    Person.call(p1,"hk","26");
    
    • Object.create创建对象
    var a = {a: 1};
    // a--> Object.prototype--> null
    
    • class关键字创建对象

    ES6引入了一套新关键字用来实现class,这些新的关键字包括classconstructorstaticextendssuperclass本质是Function

    class animal {
      constructor(name) {
        this.name = name;
      }
    }
    class duck extends animal {
      constructor() {
        super("duck");
      }
    }
    var a = new duck();
    console.log(animal instanceof Function); //true
    
  3. 继承

    • 原型链继承

      基本思想是让子类型原型对象等于超类型的实例。

      function SuperType() {
        this.property = true;
        this.colors = ["red", "blue"];
      }
      
      SuperType.prototype.getSuperValue = function () {
        return this.property;
      };
      
      function SubType() {
        this.subproperty = false;
      }
      //核心:子类型原型对象等于超类型的实例
      SubType.prototype = new SuperType();
      SubType.prototype.getSubValue = function () {
        return this.subproperty;
      };
      
      var instance = new SubType();
      console.log(instance.getSuperValue()); //true
      

      缺点:1、超类型中的引用类型值将被所有子类型实例共享。2、创建子类型实例时,不能像超类型的构造函数中传递参数。

      var instance = new SubType();
      instance.colors.push("black");
      console.log(instance.colors); //["red", "blue","black"]
      
      var instance2 = new SubType();
      console.log(instance2.colors); //["red", "blue","black"]
      
    • 借用构造函数

      基本思想是在子类型构造函数的内部调用超类型构造函数。

      function SuperType() {
        this.colors = ["red", "blue"];
      }
      
      function SubType() {
        //调用超类型构造函数
        SuperType.call(this);
      }
      
      var instance = new SubType();
      instance.colors.push("black");
      console.log(instance.colors);
      
      var instance2 = new SubType();
      console.log(instance2.colors);
      

      优点:1、解决了原型链继承问题。

      缺点:超类型原型中的方法对子类型是不可见的。

    • 组合继承

    基本思想是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

    
    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue"];
    }
    
    SuperType.prototype.sayName = function () {
      console.log(this.name);
    };
    
    function SubType(name,age) {
      //继承属性
      SuperType.call(this,name); //第二次调用SuperType()
      this.age = age;
    }
    
    SubType.prototype = new SuperType(); //第一次调用SuperType()
    SubType.prototype.sayAge = function () {
      console.log(this.age);
    };
    
    var instance = new SubType("jack",26);
    instance.colors.push("black");
    console.log(instance.colors); //[ 'red', 'blue', 'black' ]
    instance.sayName(); //jack
    instance.sayAge(); //26
    
    var instance2 = new SubType("rose",25);
    console.log(instance2.colors); //[ 'red', 'blue' ]
    instance2.sayName(); //rose
    instance2.sayAge(); //25
    

    优点:避免了原型链继承和借用构造函数继承的缺陷,融合了他们的优点,是JavaScript中最常用的继承模式。

    缺点:调用了两次超类型的构造函数,第一次调用时,SubType.prototype会得到namecolors属性,第二次调用时,会在新对象创建实例属性namecolors。于是,这两个属性就屏蔽了原型中的两个同名属性。可采用寄生组合式继承解决该问题。

this指向以及callapplybind方法

  1. callapply方法

    在函数内部,this的值取决于函数被调用的方式。如果要想把this的值从一个环境传到另一个,就要用call或者apply方法。当函数在其主体中使用this关键字时,可以通过使用函数继承自Function.prototypecallapply方法将this值绑定到调用的特定对象。

    function add(c, d) {
      console.log(this.a + this.b + c + d);
    }
    let obj = {a: 1, b: 2};
    //第一个参数是作为this使用的对象
    //call和apply的区别是参数不同
    add.call(obj, 3, 4);
    add.apply(obj, [3, 4]);
    
  2. bind方法

    调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到bind的第一个参数,无论这个函数是如何被调用的。callapply方法调用后会立即执行,bind方法不会。

    function f() {
      console.log(this.a)
    }
    let g = f.bind({a: 1});
    g(); //1
    let h = g.bind({a: 2});
    h(); //1
    
  3. 箭头函数中this指向

    箭头函数不会创建自己this,它只会从自己作用域链上一层继承this(即this与封闭词法环境中的this保持一致)。在全局代码中,它的this指向全局变量,在其他函数体内,this被设置为封闭词法环境的this

    function Person() {
      this.name = "name";
      setTimeout(function () {
        console.log(this.name + "ing"); //undefineding
        }, 0);
      //this正确指向p实例
      setTimeout(() => console.log(this.name + "growing")); //namegrowing
    }
    let p = new Person();
    
  4. 对象的方法

    当函数作为对象的方法被调用时,它的this指向调用该函数的对象。

    var obj = {
      prop: 2,
      f: function () {
        console.log(this.prop);
      }
    };
    obj.f(); //2
    
  5. 构造函数

    构造函数中this指向的是new创造的新对象。

作用域与闭包

  1. 作用域

    当函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[scope]])。然后使用this、arguments和其他命名参数的值来初始化函数的活动对象。在作用域链中,外部函数的活动对象处于第二位,外部函数的外部函数的活动对象处于第三位,直到作为作用域链终点的全局执行环境。

  2. 闭包

    闭包是由函数以及创建该函数的词法环境组合而成,这个环境包含了该闭包创建时能访问的所有局部变量。通俗理解是,函数B定义在函数A内部,且使用了函数A的局部变量,则B被称为闭包。

      function init() {
        var name = "name";
        function f() {
          console.log(name);
        }
        f();
      }
      init();
    

    闭包常见问题

      for (var i = 1; i < 4; i++) {
        setTimeout(
          function () {
            console.log(i);
          }, 0)
      }
    

    for循环是同步代码,先执行然后生成三个闭包函数,这三个函数共享同一个作用域,setTimeout是异步代码,此时for循环结束,i=4,所以会输出4、4、4。

    解决办法:

    1、使用let,每个闭包绑定的是块级作用域的变量

      for (let i = 1; i < 4; i++) {
        setTimeout(
          function () {
            console.log(i);
          }, 0)
      }
    

    2、 使用立即执行函数

    for (var i = 1; i < 4; i++) {
        (function (i) {
          setTimeout(function () {
            console.log(i);
          }, 0)
        })(i);
      }
    
  3. 性能问题

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多内存,过度使用闭包可能会导致内存占用过多。

深拷贝与浅拷贝

定义两个变量a和b,a=b,当修改b时,a不发生变化则为深拷贝,a发生变化则为浅拷贝。由数据类型可以得出,当b为引用类型数据时,会发生浅拷贝。

深拷贝实现:

  1. JSON.stringifyJson.parse实现

    function deepClone(obj) {
      let _obj = JSON.stringify(obj);
      let result = JSON.parse(_obj);
      return result;
    }
    
  2. 递归实现深拷贝

     function deepClone(obj) {
       let newobj = Array.isArray(obj) ? [] : {};
       if (obj && typeof obj === "object") {
         for (key in obj) {
           //判断是否自有属性
           if (obj.hasOwnProperty(key)) {
             //如果属性值为对象,则递归拷贝
             if (obj[key] && typeof obj[key] === "object") {
               newobj[key] = deepClone(obj[key]);
             } else {
               newobj[key] = obj[key];
             }
           }
         }
       }
       return newobj;
     }
    

防抖与节流

  • 防抖(debounce):任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。核心是对于一个时间段的连续的函数调用,只让其执行一次。

      function debounce(fn, interval = 300) {
        let timeoutid = null;
        return function () {
          clearTimeout(timeoutid);
          timeoutid = setTimeout(() => {
            fn.apply(this, arguments);
          }, interval);
        }
      }
    

    应用场景:

    1. 每次resize/scroll触发统计事件
    2. 文本输入的验证(连续输入文字后发送AJAX请求进行验证,验证一次就好)
  • 节流(throttle):指定时间间隔内只会执行一次任务。核心是让一个函数不要执行得太频繁,减少一些过快的调用来节流。

    function throttle(fn, interval = 300) {
      let canRun = true;
      return function () {
        if (!canRun) return;
        canRun = false;
        setTimeout(() => {
          fn.apply(this, arguments);
          canRun = true;
        }, interval);
      }
    }
    

    应用场景:

    1. DOM元素拖拽功能(mousemove)
    2. 搜索联想(keyup)
    3. 监听滚动事件判断是否到页面底部自动加载更多,给scroll加了debounce后,只有用户停止滚动才会判断是否到了页面底部,如果是throttle,只要页面滚动就会间隔一段时间判断一次

数组操作

  1. 数组的检测

    详见js数据类型

  2. 数组模拟栈和队列

    栈:后进先出,数组的pop()push()方法模拟

    var array = [];
    array.push("red");
    array.push("green");
    array.push("black");
    array.pop();
    

    队列:先进先出,数组的shift()push()方法模拟。(也可使用unshift()pop()方法模拟)

    var array = [];
    array.push("red","green","black");
    array.shift();
    
  3. 数组常用方法

    • contact(),数组连接,返回一个新数组。
    • slice(start,end),数组截取,返回一个新数组。
    • splice(start,num,value),数组删除、插入或替换,修改原数组
    • indexof/lastIndexof(value,fromIndex),查找数组项,返回对应的下标
    • every(),对数组的每一项运行给定函数,如果每一项都返回true,则返回true
    • filter(),对数组的每一项运行给定函数,返回该函数返回true的项组成的数组
    • forEach(),对数组的每一项运行给定函数,无返回值
    • map(),对数组的每一项运行给定函数,返回每次函数调用结果组成的数组
    • some(),对数组的每一项运行给定函数,如果任一项返回true,则返回true
    • reduce/reduceRight(fn(prev,cur,index,array)),数组缩小方法
    • reverse(),将原数组的顺序进行反转
    • sort(),按升序排列数组项
    • join(),通过指定连接符生成字符串

ES5高级技巧

  1. 判断是否原生数组、函数

    var arr = [];
    function foo() {
      return 0;
    }
    console.log(Object.prototype.toString.call(arr) == "[object Array]");//true
    console.log(Object.prototype.toString.call(foo) == "[object Function]");//true
    
  2. 作用域安全的构造函数

    当调用构造函数创建实例却未使用new运算符时,会误创造全局变量,为了解决这个问题,在构造函数内判断this是否正确对象的实例。这种写法会导致仅借用构造函数的继承原型链被破坏,加上原型链继承可解决该问题。

    function Person(name) {
      if (this instanceof Person) {
        this.name = name;
      }else {
        return new Person(name);
      }
    }
    
  3. 函数柯里化

    函数柯里化用于创建已经设置好了一个或多个参数的函数。

    function curry(fn) {
      var args = Array.prototype.slice.call(arguments, 1); //返回从第二个参数开始的所有参数
      return function () {
        var innerArgs = Array.prototype.slice.call(arguments); //返回内部函数参数
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
      }
    }
    function add(num1, num2) {
      return num1 + num2;
    }
    
    var curadd = curry(add, 2);
    console.log(curadd(3)); //5
    

    bind函数柯里化:

    function bind(fn, context) {
      var args = Array.prototype.slice.call(arguments, 2);
      return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(context, finalArgs);
      }
    }
    
  4. 防篡改对象

    • 不可扩展对象:Object.preventExtensions(obj),调用此方法后,不能给obj添加新属性和方法,但可修改和删除已有成员。(防篡改登记:低)
    • 密封对象:Object.seal(obj),密封对象不可扩展。调用此方法后,不能给obj添加新属性和方法,不能删除属性和方法。(防篡改等级:中)
    • 冻结对象:Object.freeze(obj),冻结对象既不可扩展又是密封的。调用此方法后,不能给obj添加新属性和方法,不能修改删除属性和方法。(防篡改登记:高)
  5. 使用定时器小任务分割处理大数组

    function chunk(array, process, context) {
      setTimeout(function () {
        var item = array.shift();
        process.call(context, item);
        if (array.length > 0) {
          setTimeout(arguments.callee, 100);
        }
      }, 100)
    }
    
  6. 自定义事件

    function EventTarget() {
      this.handlers = {};
    }
    
    EventTarget.prototype = {
      constructor: EventTarget,
      addHandler: function (type, handler) {
        if (typeof this.handlers[type] == "undefined") {
          this.handlers[type] = [];
        }
        this.handlers[type].push(handler);
      },
      fire: function (event) {
        if (!event.target) {
          event.target = this;
        }
        if (this.handlers[event.type] instanceof Array) {
          var handler = this.handlers[event.type];
          for (let i = 0; i < handler.length; i++) {
            handler[i](event);
          }
        }
      },
      removeHandler: function (type, handler) {
        if (this.handlers[type] instanceof Array) {
          var handlers = this.handlers[type];
          for (var i = 0; i < handlers.length; i++) {
            if (handlers[i] === handler) {
              break;
            }
          }
          handlers.splice(i, 1);
        }
      }
    };