Tiger老师

Tiger老师 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Tiger老师 发布了文章 · 4月15日

前端面试官的吐槽:问CSS、DOM的,还招不招人了?KPI还要不要了?

前言

看到一位博主吐槽怎么现在很多前端面试中,很多公司很少或不问css,而直接问JavaScript、框架、Node.js这个问题,说到现在满世界 Vue、小程序的简历,问啥 css啊,好多人 position 有啥值都不知道,z-index 值和 position 层叠关系都不知道,问了就挂,咋张嘴问…… 就更别提啥权重值怎么计算的这种稍微“难”一点点的问题了,考察更深入一点的,比如值处理步骤乃至到 CSSOM 就想都别想了。

个人是认为这说明业界前端的业务以pc系统后台类的居多。现在这些后台,不再需要兼容IE,界面有相对来说规则,而且又有postcss这类css编译工具,基本上不会有太大的css问题。但是如果一个前端团队以移动端业务开发为主的,那css可以问的问题还是很多的。

正文

但我这边是要面的。很多公司面人的问题千奇百怪,甚至是很脱离实际开发工作,我个人对这样的面试没有好感,我自己弄了一套面试题目。非常遗憾,我的问题不多,也不偏离业务开发,但面试快一年了,全答上的面试者有一个。那哥们我很喜欢,给了offer,不过别人没选我这边。其余的起码大几十个面试者,都或多或少地“翻车”,我也只能挑几个翻车不严重的人进2面。哪里翻车最严重?CSS!我只有4道CSS面试题,90%的面试者翻车。

  • 第一道:有3个div,呈竖向排列,第一个div贴顶,第三个div贴底,中间的div填满剩余空间。该怎么做?(flex,grid,js动态计算)
  • 第二道:我要写一个弹窗,需要水平竖直居中,同时它不能被其他元素遮掩。该怎么做?
    (居中,zindex,zindex的从父原则)
  • 第三道:你的移动端的自适应是怎么做的?
    (rem,vwvh。其实这两个等比放大缩小的自适应单位解决方案是不够完美的。内容用px + 排版用自适应单位 + 布局采用flex/grid才是解决移动端自适应的最优解)
  • 第四道:如何减少重排。

这4个题是真的不难,我原先的设想是搞几道css做开胃菜,慢慢深入了解面试者,然而这个翻车率是我始料未及的,特别是很多的面试者都在简历上注明:擅长/熟练使用Flex布局。想想面试那段时间都挺头疼的,比如发请求拿数据,每个前端都要做的事。问怎么区分返回内容是文件流还是json数据,其实就一个content-type,答上来的人不多。问平时怎么上传文件的?就一个form-data,很多人也不会。再问怎么看一个资源的缓存情况,一个control-cache,就更没人会了…我不想问一些httptcp、什么链路层、什么三次握手、什么https的加密规则,平时工作用不着,刻意去背一背应付面试,不见得能体现一个面试者的水平。但我问的这些这么实操的东西都不知所云,那是绝对行不通的。现在很多前端,只会跑vue-cli搭建个项目,import一下ui库,就觉得自己没问题了,实际上在我眼里连日常开发工作都应付不了。

我这种还算好的了,碰到喜欢出越深越好,越偏门面试题的,可能是真不想招人,不想要KPI,但要是换他来答题,十有八九更差劲(别杠,杠就是你对)。虽然说是问CSS少了,但不怕一万就怕万一,咱们可别因没准备吃了哑巴亏,所以我整理了一套前端面试题免费分享给大家,里面包括了CSS,大家在准备面试时,也别忘了看下CSS哈。

面试题

CSS

1.页面渲染时,dom元素所采用的布局模型,可通过box-sizing进行设置。根据计算宽高的区域可分为
2.ie盒模型算上border、padding及自身(不算margin),标准的只算上自身窗体的大小css设置方法如下
3.几种获得宽高的方式
4.拓展各种获得宽高的方式
5.边距重叠解决方案(BFC)BFC原理
6.cssreset和normalize.css有什么区别:
7.居中方法
8.css优先确定级
9.如何清除浮动
10.CSS开启GPU加速
11.开启GPU硬件加速可能触发的问题
12.CSS中link与@import的区别
13.CSS选择器列表优先级及权重
14.display:none和visibility:hidden的区别:
15.介绍一下CSS的盒子模型

HTML

1.你是怎么理解HTML语义化
2.你用过哪些HTML5标签
3.metaviewport是做什么用的,怎么写?
4.H5是什么
5.label标签的作用
6.行内元素有哪些?块级元素有哪些?空(void)元素有那
些?
7.a标签中如何禁用href跳转页面或定位链接
8.canvas在标签上设置宽高和在style中设置宽高有什么区别
9.你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么?
10.iframe有哪些缺点?

VUE

1.vue.js的两个核心是什么?
2.vue的双向绑定的原理是什么?
3.vue生命周期钩子函数有哪些?
4.请问v-if和v-show有什么区别?
5.vue常用的修饰符
6.nextTick
7.什么是vue生命周期
8.数据响应(数据劫持)
9.virtualdom原理实现
10.Proxy相比于defineProperty的优势

由于篇幅问题,只展示了一小部分,需要完整版PDF的小伙伴们点击这里就可以免费获取啦

最最后还是想说,无论我们遇到什么样的面试官,被问到什么题,你都要相信你是最棒的!祝小伙伴们在金三银四之际顺顺利利拿到心仪offer!喜欢小编这篇文章,请留下你们的点赞留言支持小编,谢谢啦~

查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月14日

Javascript的原型和原型链

构造函数创建对象:

function Person() {

}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin

Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person

prototype

每个函数都有一个 prototype 属性
每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。

function Person() {

}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin

proto

每一个JavaScript对象(除了 null )都具有的一个属性,叫proto,这个属性会指向该对象的原型

function Person() {

}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

image

constructor

每个原型都有一个 constructor 属性指向关联的构造函数 实例原型指向构造函数

function Person() {

}
console.log(Person === Person.prototype.constructor); // true

function Person() {

}

var person = new Person();

console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

实例与原型

function Person() {

}

Person.prototype.name = 'Kevin';

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin

在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。

但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。

原型与原型

var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin

原型链

console.log(Object.prototype.__proto__ === null) // true

JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些

查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月14日

Javascript的继承与多态

本文先对es6发布之前javascript各种继承实现方式进行深入的分析比较,然后再介绍es6中对类继承的支持以及优缺点讨论。最后介绍了javascript面向对象编程中很少被涉及的“多态”,并提供了“运算符重载”的思路。本文假设你已经知道或了解了js中原型、原型链的概念。

es6之前,javascript本质上不能算是一门面向对象的编程语言,因为它对于封装、继承、多态这些面向对象语言的特点并没有在语言层面上提供原生的支持。但是,它引入了原型(prototype)的概念,可以让我们以另一种方式模仿类,并通过原型链的方式实现了父类子类之间共享属性的继承以及身份确认机制。其实,面向对象的概念本质上来讲不是指某种语言特性,而是一种设计思想。如果你深谙面向对象的编程思想,即使用c这种面向过程的语言也能写出面向对象的代码(典型的代表就是windows NT 内核实现),而javascript亦是如此!正是由于javascript本身对面向对象编程没有一个语言上的支持标准,所以才有了五花八门、令人眼花缭乱的“类继承”的代码。所幸,es6增加了class、extends、static等关键字用以在语言层面支持面向对象,但是,还是有些保守!我们先列举出es6之前常见的几种继承方案,然后再来一探es6的类继承机制,最后再讨论下javascript多态。

ES6之前的继承

原型赋值方式

简而言之,就是直接将父类的一个实例赋给子类的原型。如下示例:

function Person(name){
 this.name=name;
 this.className="person" 
}
Person.prototype.getClassName=function(){
 console.log(this.className)
}

function Man(){
}

Man.prototype=new Person();//1
//Man.prototype=new Person("Davin");//2
var man=new Man;
>man.getClassName()
>"person"
>man instanceof Person
>true

如代码中1处所示,这种方法是直接new 了一个父类的实例,然后赋给子类的原型。这样也就相当于直接将父类原型中的方法属性以及挂在this上的各种方法属性全赋给了子类的原型,简单粗暴!我们再来看看man,它是Man的一个实例,因为man本身没有getClassName方法,那么就会去原型链上去找,找到的是person的getClassName。这种继承方式下,所有的子类实例会共享一个父类对象的实例,这种方案最大问题就是子类无法通过父类创建私有属性。比如每一个Person都有一个名字,我们在初始化每个Man的时候要指定一个不同名字,然后子类将这个名字传递给父类,对于每个man来说,保存在相应person中的name应该是不同的,但是这种方式根本做不到。所以,这种继承方式,实战中基本不用!

调用构造函数方式

function Person(name){
 this.name=name;
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
var man1=new Man("Davin");
var man2=new Man("Jack");
>man1.name
>"Davin"
>man2.name
>"Jack"
>man1.getName() //1 报错
>man1 instanceof Person
>true

这里在子类的在构造函数里用子类实例的this去调用父类的构造函数,从而达到继承父类属性的效果。这样一来,每new一个子类的实例,构造函数执行完后,都会有自己的一份资源(name)。但是这种办法只能继承父类构造函数中声明的实例属性,并没有继承父类原型的属性和方法,所以就找不到getName方法,所以1处会报错。为了同时继承父类原型,从而诞生了组合继承的方式:

组合继承

function Person(name){
 this.name=name||"default name"; //1
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
//继承原型
Man.prototype = new Person();
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"

这个例子很简单,这样不仅会继承构造函数中的属性,也会复制父类原型链中的属性。但是,有个问题,Man.prototype = new Person(); 这句执行后,Man的原型如下:

> Man.prototype
> {name: "default name", className: "person"}

也就是说Man的原型中已经有了一个name属性,而之后创建man1时传给构造的函数的name则是通过this重新定义了一个name属性,相当于只是覆盖掉了原型的name属性(原型中的name依然还在),这样很不优雅。

分离组合继承

这是目前es5中主流的继承方式,有些人起了一个吊炸天的名字“寄生组合继承”。首先说明一下,两者是一回事。分离组合继承的名字是我起的,一来感觉不装逼会好点,二来,更确切。综上所述,其实我们可以将继承分为两步:构造函数属性继承和建立子类和父类原型的链接。所谓的分离就是分两步走;组合是指同时继承子类构造函数和原型中的属性。

function Person(name){
 this.name=name; //1
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
//注意此处
Man.prototype = Object.create(Person.prototype);
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"

这里用到了Object.creat(obj)方法,该方法会对传入的obj对象进行浅拷贝。和上面组合继承的主要区别就是:将父类的原型复制给了子类原型。这种做法很清晰:

  1. 构造函数中继承父类属性/方法,并初始化父类。
  2. 子类原型和父类原型建立联系。

还有一个问题,就是constructor属性,我们来看一下:

> Person.prototype.constructor
< Person(name){
   this.name=name; //1
   this.className="person" 
 }
> Man.prototype.constructor
< Person(name){
   this.name=name; //1
   this.className="person" 
  }

constructor是类的构造函数,我们发现,Person和Man实例的constructor指向都是Person,当然,这并不会改变instanceof的结果,但是对于需要用到construcor的场景,就会有问题。所以一般我们会加上这么一句:

Man.prototype.constructor = Man

综合来看,es5下,这种方式是首选,也是实际上最流行的。

行文至此,es5下的主要继承方式就介绍完了,在介绍es6继承之前,我们再往深的看,下面是独家干货,我们来看一下Neat.js中的一段简化源码:

//下面为Neat源码的简化
-------------------------
function Neat(){
  Array.call(this)
}
Neat.prototype=Object.create(Array.prototype)
Neat.prototype.constructor=Neat
-------------------------
//测试代码
var neat=new Neat;
>neat.push(1,2,3,4)
>neat.length //1
>neat[4]=5
>neat.length//2
>neat.concat([6,7,8])//3

现在提问,上面分割线包起来的代码块干了件什么事?

对,就是定义了一个继承自数组的Neat对象!下面再来看一下下面的测试代码,先猜猜1、2、3处执行的结果分别是什么?期望的结果应该是:

4
5
1,2,3,4,5,6,7,8

而实际上却是:

4
4
[[1,2,3,4],6,7,8]

呐尼!这不科学啊 !why ?

我曾在阮一峰的一篇文章中看到的解释如下:

因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。ES5是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。

然而,事实并非如此!确切来说,并不是原生构造函数会忽略掉apply方法传入的this而导致属性无法绑定。要不然1处也不会输出4了。还有,neat依然可以正常调用push等方法,但继承之后原型上的方法有些也是有问题的,如neat.concat。其实可以看出,我们通过Array.call(this)也是有用的,比如length属性可用。但是,为什么会出问?根据症状,可以肯定的是最终的this肯定有问题,但具体是什么问题呢?难道是我们漏了什么地方导致有遗漏的属性没有正常初始化?或者就是浏览器初始化数组的过程比较特殊,和自定义对象不一样?首先我们看第一种可能,唯一漏掉的可能就是数组的静态方法(上面的所有继承方式都不会继承父类静态方法)。我们可以测试一下:

for(var i in  Array){
 console.log(i,"xx")
}

然而并没有一行输出,也就是说Array并没有静态方法。当然,这种方法只可以遍历可枚举的属性,如果存在不可枚举的属性呢?其实即使有,在浏览器看来也应该是数组私有的,浏览器不希望你去操作!所以第一种情况pass。那么只可能是第二种情况了,而事实,直到es6出来后,才找到了答案:

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

请注意我加粗的文字。“所有”,这个词很微妙,不是“没有”,那么言外之意就是说es5是部分了。根据我之前的测试(在es5下),下标操作和concat在chrome下是有问题的,而大多数函数都是正常的,当然,不同浏览器可能不一样,这应该也是jQuery每次操作后的结果集以一个新的扩展后的数组的形式返回而不是本身继承数组(然后再直接返回this的)的主要原因,毕竟jQuery要兼容各种浏览器。而Neat.js面临的问题并没有这么复杂,只需把有坑的地方绕过去就行。言归正传,在es5中,像数组一样的,浏览器不让我们愉快与之玩耍的对象还有:

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

es6的继承方式

es6引入了class、extends、super、static(部分为ES2016标准)

class Person{
  //static sCount=0 //1
  constructor(name){
     this.name=name; 
     this.sCount++;
  }
  //实例方法 //2
  getName(){
   console.log(this.name)
  }
  static sTest(){
    console.log("static method test")
  }
}

class Man extends Person{
  constructor(name){
    super(name)//3
    this.sex="male"
  }
}
var man=new Man("Davin")
man.getName()
//man.sTest()
Man.sTest()//4
输出结果:
Davin
static method test

ES6明确规定,Class内部只有静态方法,没有静态属性,所以1处是有问题的,ES7有一个静态属性的提案,目前Babel转码器支持。熟悉java的可能对上面的代码感觉很亲切,几乎是自解释的。我们大概解释一下,按照代码中标号对应:

  1. constructor为构造函数,一个类有一个,相当于es5中构造函数标准化,负责一些初始化工作,如果没有定义,js vm会定义一个空的默认的构造函数。
  2. 实例方法,es6中可以不加"function"关键字,class内定义的所有函数都会置于该类的原型当中,所以,class本身只是一个语法糖。
  3. 构造函数中通过super()调用父类构造函数,如果有super方法,需要时构造函数中第一个执行的语句,this关键字在调用super之后才可用。
  4. 静态方法,在类定义的外部只能通过类名调用,内部可以通过this调用,并且静态函数是会被继承的。如示例中:sTest是在Person中定义的静函数,可以通过Man.sTest()直接调用。

es6和es5继承的区别

大多数浏览器的ES5实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类B__proto__属性指向父类A,子类Bprototype属性的__proto__属性指向父类Aprototype属性。

这样的结果是因为,类的继承是按照下面的模式实现的:

class A {
}

class B {
}

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B继承A的静态属性
Object.setPrototypeOf(B, A);

Object.setPrototypeOf的简单实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

因此,就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

es6继承的不足

  1. 不支持静态属性(除函数)。
  2. class中不能定义私有变量和函数。class中定义的所有函数都会被放倒原型当中,都会被子类继承,而属性都会作为实例属性挂到this上。如果子类想定义一个私有的方法或定义一个private 变量,便不能直接在class花括号内定义,这真的很不方便!

总结一下,和es5相比,es6在语言层面上提供了面向对象的部分支持,虽然大多数时候只是一个语法糖,但使用起来更方便,语意化更强、更直观,同时也给javascript继承提供一个标准的方式。还有很重要的一点就是-es6支持原生对象继承。

多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。这是标准定义,在c++中实现多态的方式有虚函数、抽象类、模板,在java中更粗暴,所有函数都是“虚”的,子类都可以重写,当然java中没有虚函数的概念,我们暂且把相同签名的、子类和父类可以有不同实现的函数称之为虚函数,虚函数和模版(java中的范型)是支持多态的主要方式,因为javascript中没有模版,所以下面我们只讨论虚函数,下面先看一个例子:

function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.toString=function(){
 return "I am a Person, my name is "+ this.name
}
function Man(name,age){
  Person.apply(this,arguments)
}
Man.prototype = Object.create(Person.prototype);
Man.prototype.toString=function(){
  return "I am a Man, my name is"+this.name;
}
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
> person+""
> "I am a Person, my name is Neo"
> man1+""
> "I am a Man, my name isDavin"
> man1<man2 //期望比较年龄大小 1
> false

上面例子中,我们分别在子类和父类实现了toString方法,其实,在js中上述代码原理很简单,对于同名函数,子类会覆父类的,这种特性其实就是虚函数,只不过js中不区分参数个数,也不区分参数类型,只看函数名称,如果名称相同就会覆盖。现在我们来看注释1,我们期望直接用比较运算符比较两个man的大小(按年龄),怎么实现?在c++中有运算符重载,但java和js中都没有,所幸的是,js可以用一种变通的方法来实现:

function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.valueOf=function(){
 return this.age
}
function Man(name,age){
  Person.apply(this,arguments)
}

Man.prototype = Object.create(Person.prototype);
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
var man3=new Man("Joe",19)

>man1<19//1
>true
>person==19//2
>true
>man1<man2//3
>true
>man2==man3 //4 注意
>true
>person==man2//5
>false

其中1、2、3、5在所有js vm下结果都是确定的。但是4并不一定!javascript规定,对于比较运算符,如果一个值是对象,另一个值是数字时,会先尝试调用valueOf,如果valueOf未指定,就会调用toString;如果是字符串时,则先尝试调用toString,如果没指定,则尝试valueOf,如果两者都没指定,将抛出一个类型错误异常。如果比较的两个值都是对象时,则比较的时对象的引用地址,所以若是对象,只有自身===自身,其它情况都是false。现在我们回过头来看看示例代码,前三个都是标准的行为。而第四点取决于浏览器的实现,如果严格按照标准,这应该算是chrome的一个bug ,但是,我们的代码使用时双等号,并非严格相等判断,所以浏览器的相等规则也会放宽。值得一提的是5,虽然person和man2 age都是19,但是结果却是false。总结一下,chrome对相同类的实例比较策略是先会尝试转化,然后再比较大小,而对非同类实例的比较,则会直接返回false,不会做任何转化。 所以我的建议是:如果数字和类实例比较,永远是安全的,可以放心玩,如果是同类实例之间,可以进行非等比较,这个结果是可以保证的,不要进行相等比较,结果是不能保证的,一般相等比较,变通的做法是:

var equal= !(ob1<ob2||ob1>ob2) 
//不小于也不大于,就是等于,前提是比较操作符两边的对象要实现valueOf或toString

当然类似toString、valueOf的还有toJson方法,但它和重载没有什么关系,故不冗述。

数学运算符

让对象支持数学运算符本质上和让对象支持比较运算符原理类似,底层也都是通过valueOf、toString来转化实现。但是通过这种覆盖原始方法模拟的运算符重载有个比较大局限就是:返回值只能是数字!而c++中的运算符重载的结果可以是一个对象。试想一下,如果我们现在要实现一个复数类的加法,复数包括实部与虚部,加法要同时应用到两个部分,而相加的结果(返回值)仍然是一个复数对象,这种情况下,javascript也就无能为力了。

查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月13日

怎样判断面试者是否有扎实的前端基础?

前言

最近朋友去面试,得到反馈说 ta 的“前端基础”不好。我突然觉得这是一个很有趣的问题,比如对于武术家来说,你让他扎一个小时马步,就知道他基础好不好;对于歌手,你让他视唱视奏一下,就知道他基础好不好。

而技术面试,大家往往都是最开始面的几家公司会炮灰,然后随着面试过程不断刷题或者背概念,在背下来 HTTP协议概念,网络攻击,异步标准之后,终于在面到第 N 家的时候拿到了 offer。

问题是,短短几个礼拜的时间其实并不会增加多少知识和经验,但是就能轻易的从“基础不好”变成”基础扎实“,到底是我们对”基础“的定义有问题,还是对”基础“的评价标准有问题?

按理说基础的考察应该是可以有一个开放标准的,就是无论是否提前知道题目,都几乎不会影响获得的分数,那么对于前端来说,什么是长期积累下才能夯实的基础呢?我们在面试的时候又应该怎么量化去评判它?

一般大家会遇到那种问的问题去google就能知道答案的知识,尤其是在面试初级前端时。其实面对这种面试官很简单:
1.刷题
2.把上个面试官问的问题都记下来,回去查清楚,如此反复,就能应付这帮面试官了。

其实说到底,面试官看重的重点就两个:
1.这个面试者目前知道哪些知识
2.这个面试者未来能否搞定我们的工作

大部分面试官会选1,而我,可能也是跟我经历和特点有关吧,我个人会更关注2。我面试过不少应届毕业生,都是中国不错的大学。我发现我跟其他面试官的理念差异很大,有的二面面试官问我,这个人连 AMD 都没听说过,你怎么不把他拒了。嗯?AMD 很重要吗,不知道就要拒吗?这东西不就是两个函数约定吗?在指导下学起来很快啊。没踩过坑的不要?我踩过啊,你们团队没有code review 吗?其实是因为我更看重面试者其他方面的特质。前端这些基础,说实话,教两个月基本就会了。如果你从来没打算教新人就当我没说。我更关心的他英语水平如何,他是否有编程思维,他是否能跟我有效沟通,他遇到问题是否会立即反馈给我而不是闷头干,他是否能快速掌握目前不会的知识,他是否会被团队里的人喜欢。还有就是,说句不好听的,有些前端团队不好好培训新人,尽想着用新人的价格招一个中级前端,这些团队也是想瞎了心了,早点死心吧。

判断

回到题目,如果你真想检验一个人的水平。第一步先考察一下基本的编程基础,问几个基本的编程问题,可以和前端相关也可以无关。比如垃圾收集大致是怎么做的,setTimeout 大致做了什么(说会在另一个线程里执行回调的直接毙掉)。

第二步考察一下知识面,问问http、tcp的基本知识,dns是怎么工作的,或者常用框架的实现原理,看看候选人是不是除了自己的一亩三分地什么都不关心。

第三步考察hold业务逻辑的能力,从一个简单的注册页,或者查询页开始,先让说下代码的基本架构,然后需求、性能、可靠性、安全层层加码,看看能不能很快的反馈出解决方案。能对答如流的要么做过,要么对他来说这种复杂度的东西是小case。

前三步都没问题,基本上说明候选人已经还行了,但是行到什么程度,不知道。如果想找比较厉害的,就增加个第四步,亮点项目考察。

总的来说,面试官要是考察思路就会从你实际做过的项目入手,考察你实际编码能力,就会让你在电脑敲代码,看你用什么编辑器、插件、编码习惯等。所以我们在回答面试官问题时,有一个清晰的逻辑思路,清楚知道自己在和面试官说项目说技术时的话就好了,我整理一套前端面试题,免费分享给大家,希望对即将去面试的小伙伴们有帮助!

由于篇幅原因,如有需要以上完整学习笔记PDF,可以点击这里免费自取
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~小编在这谢谢大家啦!

更多前端学习相关资料点此处免费领取

查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月12日

js构造函数详解

一、js函数

  首先弄明白何为函数呢,按照犀牛书的说法,函数简单的说就是重复执行的代码块。函数是这样的一段JavaScript 代码,它只定义一次,但可能被执行或调用任意次。

函数的定义方式:

    1.声明式函数定义: function 函数名 (){};这种定义方式,会将函数声明提升到该函数所在作用域的最开头,也是就无论你在这个函数的最小作用域的那儿使用这种方式声明的函数,在这个作用域内,你都可以调用这个函数为你所用。

    2.函数表达式:let fun = function(){}; 此方式定义的函数,只能在该作用域中,这段赋值代码执行之后才能通过fun()调用函数,否则,由于变量声明提升,fun === undefined。

    3.new Function 形式: var fun1 = new Function (arg1 , arg2 ,arg3 ,…, argN , body );Function构造函数所有的参数都是字符串类型。除了最后一个参数, 其余的参数都作为生成函数的参数即形参。这里可以没有参数。最后一个参数, 表示的是要创建函数的函数体。

    总结:1 、第一种和第二种函数的定义的方式其实是第三种new Function 的语法糖,当我们定义函数时候都会通过 new Function 来创建一个函数,只是前两种为我们进行了封装,我们看不见了而已,js 中任意函数都是Function 的实例。2、ECMAScript 定义的 函数实际上是功能完整的对象。

二、构造函数

  定义:通过 new 函数名 来实例化对象的函数叫构造函数。任何的函数都可以作为构造函数存在。之所以有构造函数与普通函数之分,主要从功能上进行区别的,构造函数的主要 功能为 初始化对象,特点是和new 一起使用。new就是在创建对象,从无到有,构造函数就是在为初始化的对象添加属性和方法。构造函数定义时首字母大写(规范)。

  对new理解:new 申请内存, 创建对象,当调用new时,后台会隐式执行new Object()创建对象。所以,通过new创建的字符串、数字是引用类型,而是非值类型。

  1、常用的构造函数:

    1. var arr = []; 为 var arr = new Array(); 的语法糖。

    2. var obj = {} 为 var obj = new Object(); 的语法糖

    3. var date = new Date();

    4. ...

  2、执行构造函数时发生的事 :

 let f = new Foo();

      function Foo(name,age,sex){

        this.name = name;

        this.age = age;

        this.sex = sex;

      }

      Foo.prototype.belief = function(){

        console.log('量变是质变的必要准备,质变是量变积累到一定程度的必然结果!');

      }

      let f = new Foo ('zh',18,'男');

    a .   let   f = {};   //一个继承自 Foo.prototype 的新对象被创建。

    b.   f.__proto__ = Foo.prototype; // f 继承 Foo的原型。   

    b   Foo.call(f,'zh',18,'男');    //执行Foo函数,将name,age,sex 参数传入Foo中执行,此时函数内部this 为 new 创建的 f对象,所以  f.name = 'zh';f.age = 18; f.sex = '男';

    c.  实例化对象完成,此时  f = {

                name:'zh',

                age:18,

                sex:'男'

              }

    d.   f.belief();     打印'量变是质变的必要准备,质变是量变积累到一定程度的必然结果!

    手写new函数  

    function newTest (constructFunction){
      let obj = {};
      obj.__proto__ = constructFunction.prototype;
      return function(){
        constructFunction.apply(obj,arguments);
        return obj;
      }
    }

   注意:当构造函数中有返回对象时候,最终new出来的对象会是构造函数的返回值,而不是new过程中生成的对象。仅当构造函数返回值是对象时有效,当不是对象时依旧返回new过程中形成的对象(无论如何new 构造函数之后都会返回一个对象值)。

三、ES6 中 class 与构造函数的关系

  class 为 构造函数的语法糖,即 class 的本质是 构造函数。class的继承 extends 本质 为构造函数的原型链的继承。

  例如:

  类的写法

  class Person{  //定义一个名字为Person的类

    constructor(name,age){ //constructor是一个构造方法,用来接收参数

      this.name = name;  //this代表实例对象

      this.age = age;

    } 

    say(){  //这是一个类的方法,注意千万不要加上function

      return   this.name + this.age

    }

  }

  var obj = new Person('老铁',18);

  console.log(obj.say());

  构造函数的写法

    function Person(name,age){   //构造函数和实例化构造名相同且大写(非强制,但这么写有助于区分构造函数和普通函数)

      if(!(this instanceof Person)){ //避免使用者不小心讲Person当作普通函数执行

         throw new Error(''请使用 new Person"); //仿ES6 class 中的写法

      }

      this.name = name;

      this.age = age;

    }

    Person.prototype.say = function(){

      return   this.name + this.age

    }

    

  var obj = new Person('老铁',18);   //通过构造函数创建对象,必须使用new运算符

  console.log(obj.say());

  总结:通过class定义的类 和通过构造函数定义的类 二者本质相同。并且在js执行时,会将第一种转会为第二种执行。所以 ES6 class的写法实质就是构造函数。

查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月12日

js中的instanceof运算符

概述

instanceof运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上

语法

obj instanceofObject;//true 实例obj在不在Object构造函数中

描述

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

实例

1.instanceof的普通的用法,obj instanceof Object 检测Object.prototype是否存在于参数obj的原型链上。
Person的原型在p的原型链中

functionPerson(){};
var p =new Person();
console.log(p instanceof Person);//true

2.继承中判断实例是否属于它的父类
Student和Person都在s的原型链中

functionPerson(){};
functionStudent(){};
var p =new Person();
Student.prototype=p;//继承原型var s=new Student();
console.log(s instanceof Student);//trueconsole.log(s instanceof Person);//true

3.复杂用法
这里的案例要有熟练的原型链的认识才能理解

function Person() {}
console.log(Object instanceof Object);     //true
//第一个Object的原型链:Object=>
//Object.__proto__ => Function.prototype=>Function.prototype.__proto__=>Object.prototype
//第二个Object的原型:Object=> Object.prototype

console.log(Function instanceof Function); //true
//第一个Function的原型链:Function=>Function.__proto__ => Function.prototype
//第二个Function的原型:Function=>Function.prototype

console.log(Function instanceof Object);   //true
//Function=>
//Function.__proto__=>Function.prototype=>Function.prototype.__proto__=>Object.prototype
//Object => Object.prototype

console.log(Person instanceof Function);      //true
//Person=>Person.__proto__=>Function.prototype
//Function=>Function.prototype

console.log(String instanceof String);   //false
//第一个String的原型链:String=>
//String.__proto__=>Function.prototype=>Function.prototype.__proto__=>Object.prototype
//第二个String的原型链:String=>String.prototype

console.log(Boolean instanceof Boolean); //false
//第一个Boolean的原型链:Boolean=>
//Boolean.__proto__=>Function.prototype=>Function.prototype.__proto__=>Object.prototype
//第二个Boolean的原型链:Boolean=>Boolean.prototype

console.log(Person instanceof Person); //false
//第一个Person的原型链:Person=>
//Person.__proto__=>Function.prototype=>Function.prototype.__proto__=>Object.prototype
//第二个Person的原型链:Person=>Person.prototype

总结

对应上述规范做个函数模拟A instanceof B:

function_instanceof(A, B){
    var O = B.prototype;// 取B的显示原型
    A = A.__proto__;// 取A的隐式原型while (true) {
        //Object.prototype.__proto__ === nullif (A === null)
            returnfalse;
        if (O === A)// 这里重点:当 O 严格等于 A 时,返回 truereturntrue;
        A = A.__proto__;
    }
}
查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月11日

前端面试之问到promise怎么办?

前言

Promise作为面试中的经典考题,我们一定要深刻学习和理解它! Promise有什么用呢?答:我们拿它解决异步回调问题。Pomise是ES6里面新增的一种异步编程的解决方案。现在这个promise在面试中感觉就像“css清除浮动”一样,属于答不上来就会挂掉的前端基础知识了。

本文大纲:

1.promise基本用法;

2.promise A+手动实现

3.promise 使用中的哪些坑 promise.all 回调地狱等;

(一)promise基本用法

promise对象简单的来说,有点类似于ajax,它可以看做是一个装有某个未来才会结束的事件的容器。它有两个特点:1,promise的状态不受外部影响;2.promise的状态一旦改变,就不会再变了。

可以先看下promise的结构

function Promise(executor){

var self = this

self.status = 'pending' // Promise当前的状态

self.data = undefined // Promise的值

self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面

self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面

executor(resolve, reject) // 执行executor并传入相应的参数

}

promise的结构简单来说就是,它有个status表示三种状态,pending(挂起),resolve(完成),reject(拒绝)。除此之外,它还有两个回调方法onResolvedCallbackonRejectedCallback,分别对应resolve(完成) 和reject(拒绝)。

假如前面面试官问你promise的概念和基本用法,你像我一样提到了ajax的话,面试官很可能就会顺口问一下ajax(毕竟也是前端应该掌握得基础知识之一)。根据我的经验来看,一线大厂的面试官这个时候很可能会要你用promise撸一个ajax出来,一来可以考察你promise和ajax掌握得怎么样,二来可以考察你的代码能力。

1.1用promise来实现Ajax

const $ = (function(){

const ajax = function(url, async = false, type = 'GET'){

const promise = new Promise(function(resolve, reject){

const handler = function(){

if(this.readyState !== 4){

return;

}

if(this.status === 200){

resolve(this.response);

}else{

reject(new Error(this.statusText));

}

}

const client = new XMLHttpRequest();

client.open(type, url);

client.onreadystatechange = handler;

client.responseType = 'json';

client.setRequestHeader("Accept", "application/json");

client.send();

})

return promise;

}

return {

ajax

};

})()

调用方式:

$.ajax("/posts.json").then(function(json) {

console.log('Contents: ' + json);

}, function(error) {

console.error('出错了', error);

});

(二)手动实现一个 Promise/A

要注意的几个点:1.then方法会返回一个新的promise,因此then方法应该写到原型链上。2.promise 的返回值或者抛出的err 会有传递现象。

例如:

new Promise(resolve=>resolve(8))
.then()
.catch()
.then(function(value) {
alert(value)
})

// 根据promise的定义和调用方式,可以先写出promise的数据结构

function Promise(executor){

const _this = this;

_this.status = 'pending';

_ths.data = undefined;

_this.onRejectedCallback = [];

_this.onResolvedCallback = [];

function resolve(value){

if(_this.status === 'pending'){

_this.status = 'resolved';

_this.data = value;

for(let i=0;i<_this.onResolvedCallback.length;i++){

_this.onResolvedCallback[i](value);

}

}

}

function reject(reason){

if(_this.status === 'pending'){

_this.status = 'rejected';

_this.data = reason;

for(let i=0;i<_this.onResolvedCallback.length;i++){

_this.onRejectedCallback[i](reason);

}

}

}

try{

executor(resolve, reject);

}catch (e){

reject(e)

}

}

// then方法应该写在原型链上

Promise.prototype.then = function(onResolved, onRejected){

const self = this;

// 要判断onResolved 和 onRejected是不是方法

onResolved = typeof onResolved === 'function' ? onResolved : function(value) { return value }

onRejected = typeof onRejected === 'function' ? onRejected : function(reason) { return reason }

if(self.status === 'resolved'){

return new Promise(function(resolve, reject){

try{

const resoult = onResolved([self.data](https://link.zhihu.com/?target=http%3A//self.data));

if( resoult instanceof Promise ){ // 如果返回的是新的promise,那么用这个promise的痛恨方法

resoult.then(resolve, reject)

}

resolve(resoult) // 否则 直接讲返回值作为newPromise的结果

}.catch(e){

reject(e);

}

});

}

// 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数,就不再做过多解释

if (self.status === 'rejected') {

return new Promise(function(resolve, reject) {

try {

var resoult = onRejected([self.data](https://link.zhihu.com/?target=http%3A//self.data))

if (resoult instanceof Promise) {

resoult.then(resolve, reject)

}

} catch (e) {

reject(e)

}

})

}

if(self.status === 'pending'){

return new Promise(function(){});

}

if (self.status === 'pending') {

// 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,

// 只能等到Promise的状态确定后,才能确实如何处理。

// 所以我们需要把我们的**两种情况**的处理逻辑做为callback放入promise1(此处即this/self)的回调数组里

// 逻辑本身跟第一个if块内的几乎一致,此处不做过多解释

return Promise(function(resolve, reject) {

self.onResolvedCallback.push(function(value) {

try {

var x = onResolved([self.data](https://link.zhihu.com/?target=http%3A//self.data))

if (x instanceof Promise) {

x.then(resolve, reject)

}

} catch (e) {

reject(e)

}

})

self.onRejectedCallback.push(function(reason) {

try {

var x = onRejected([self.data](https://link.zhihu.com/?target=http%3A//self.data))

if (x instanceof Promise) {

x.then(resolve, reject)

}

} catch (e) {

reject(e)

}

})

})

}

}

(三)promise 的使用中应该要避免哪些坑

在面试的时候,如果能答出来promise的使用中可能会出现什么坑,已经如何避免这些坑。相信能够给面试官一个好印象,尤其是面试2年工作经验的岗位的时候,通过这些就能很好的和培训班毕业的假简历区分开来。而且这些注意的点也是我本人在项目中实实在在踩过的坑。

注意点1,不要刻意为了美化代码而避免使用嵌套结构。

很多promise的科普教程里,作者都会强调,为了代码的间接性,尽量不要使用嵌套结构。ES7里还为了处理这个事情,专门设计了async await语法。但很多新手,再没有充分理解业务的前提下,盲目的为了美化代码,为了避免“回调地狱”,经常会造成很大的问题。

1.1promis.all 的坑

设想一个场景,比如我们写的一个表单组件。如下图所示:

这里有三个选项组件,每个组件都对应一个字典。当时组里的一个实习生,为了简洁美化代码,在这样一个组件里使用的Promise.all()。

类似于这样:

fetchData1 = function (){ // 请求组件1的字典};

fetchData2 = function (){ // 请求组件2的字典};

fetchData3 = function (){ // 请求组件3的字典};

Promise.all([fetchData1, fetchData2, fetchData3 ]);

当时看这段代码,并没有发现什么问题。结果后来一生产环境,问题就出来,控件1硬是获取不到字典项,但是获取组件1字典的接口,怎么查都是好的。最后只能重新看一遍组件的源代码,才发现了问题。原理是控件2的接口出现了问题,导致于整个Promise.all请求报错。

所以,在使用promise.all的时候要注意:业务上没有必然关联的请求比如联动组件这种,一定不要使用promise.all。

2.回调地狱并不可怕,不要盲目的使用async await

下面是比较常见的前端代码:

asycn ()=>{

await 获取订单1的数据;

await 获取订单2的数据;

......

}

当订单2的数据与订单1的数据直接没有相互依赖的关系的时候。获取订单2的执行时间就多了一倍的订单1的时间。同样的道理,假如后面还有订单3,订单4,那浪费的时间就更多了。这也是会造成前端页面卡顿的主要原因。

面对这样的常考题型,我觉得也要认真对待,因为面试中如果仅仅只是背答案,也很可能会挂掉。假如面试官看出来你在背答案,他只需要把相关的知识点都问一下,或者让你手动实现一下,又或者问你在项目中遇到了什么坑,你是怎么处理的。准备不充分的面试者,一下子就会露出马脚。

所以小编为大家准备了前端面试题资料,之前小编把前端面试问题的知识点整理成PDF文档,方便自己查阅学习,现在免费分享给大家,小编新建了一个前端学习圈,欢迎大家加入学习聊天哦~希望大家在里面有所收获也聊得开心!小伙伴们点击这里进群玩并取资料哦!
image.png

image.png

最近和小伙伴聊天了解到面试问题问vue比较多些,小编把vue相关的面试资料一起分享给大家,也是一样点击这里免费获取哦!
image.png

image.png

篇幅有限,小编没有展示完,需要文章中出现的全部资料的,点击获取来源:前端面试题资料就好喽,祝各位能在自己的人生找到属于自己的职场生涯!

查看原文

赞 1 收藏 1 评论 0

Tiger老师 发布了文章 · 4月9日

JavaScript事件的绑定与取消

定义

DOM事件被发送用于通知代码相关的事情已经发生了。每个事件都是继承自Event 类的对象,可以包括自定义的成员属性及函数用于获取事件发生时相关的更多信息。事件可以表示从基本用户交互到渲染模型中发生的事件的自动通知的所有内容。

绑定事件的方法

1.ele.onxxx = function (event) {}
    兼容性很好,但是一个元素只能绑定一个处理程序 基本等同于写在HTML行间上

2.ele.addEventListener(type, fn, false);
    IE9以下不兼容,可以为一个事件绑定多个处理程序
    当第三个参数设置为true就在捕获过程中执行,反之就在冒泡过程中执行处理函数。

3.ele.attachEvent(‘on’ + type, fn);
    IE独有,一个事件同样可以绑定多个处理程序

事件处理程序的运行环境

1.ele.onxxx = function (event) {}                       程序this指向是dom元素本身

2.obj.addEventListener(type, func, false);        程序this指向是dom元素本身

3.obj.attachEvent(‘on’ + type, fn);                     程序this指向window

封装兼容性的 addEvent(elem, type, handle) 方法

function addEvent(elem, type, handle) {

if(elem.addEventListener){
    elem.addEventListener(type, handle, false);
} else if(elem.attachEvent) {
    elem.attachEvent('on'+ type, function(){
        handle.call(elem);
    })
} else {
    elem['on' + type] = handle;
}

}

解除事件处理程序

ele.onclick = false/‘’/null;
ele.removeEventListener(type, func, false);
ele.detachEvent(‘on’ + type, func);
注:若绑定匿名函数,则无法解除

查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月9日

JavaScript事件流

一、事件

事件是文档或者浏览器窗口中发生的,特定的交互瞬间。

事件是用户或浏览器自身执行的某种动作,如click,load和mouseover都是事件的名字。

事件是javaScript和DOM之间交互的桥梁。

你若触发,我便执行——事件发生,调用它的处理函数执行相应的JavaScript代码给出响应。

典型的例子有:页面加载完毕触发load事件;用户单击元素,触发click事件。

二、事件流

事件流描述的是从页面中接收事件的顺序。

1、事件流感性认识

问题:单击页面元素,什么样的元素能感应到这样一个事件?

答案:单击元素的同时,也单击了元素的容器元素,甚至整个页面。

例子:有三个同心圆, 给每个圆添加对应的事件处理函数,弹出对应的文字。单击最里面的圆,同时也单击了外面的圆,所以外面圆的click事件也会被触发。

 效果如下:

image.png

2、事件流

事件发生时会在元素节点与根节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流。

1、两种事件流模型

事件传播的顺序对应浏览器的两种事件流模型:捕获型事件流和冒泡型事件流。

冒泡型事件流:事件的传播是从最特定事件目标到最不特定的事件目标。即从DOM树的叶子到根。【推荐】

捕获型事件流:事件的传播是从最不特定事件目标到最特定的事件目标。即从DOM树的根到叶子。

事件捕获的思想就是不太具体的节点应该更早接收到事件,而最具体的节点最后接收到事件。

<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;"><!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="myDiv">Click me!</div>
</body>
</html></pre>

上面这段html代码中,单击了页面中的<div>元素,

在冒泡型事件流中click事件传播顺序为<div>—》<body>—》<html>—》document

在捕获型事件流中click事件传播顺序为document—》<html>—》<body>—》<div>

 

note:

1)、所有现代浏览器都支持事件冒泡,但在具体实现中略有差别:

IE5.5及更早版本中事件冒泡会跳过<html>元素(从body直接跳到document)。

IE9、Firefox、Chrome、和Safari则将事件一直冒泡到window对象。

2)、IE9、Firefox、Chrome、Opera、和Safari都支持事件捕获。尽管DOM标准要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。

3)、由于老版本浏览器不支持,很少有人使用事件捕获。建议使用事件冒泡

2、DOM事件流

DOM标准采用捕获+冒泡。两种事件流都会触发DOM的所有对象,从document对象开始,也在document对象结束。

DOM标准规定事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

  • 事件捕获阶段:实际目标(<div>)在捕获阶段不会接收事件。也就是在捕获阶段,事件从document到<html>再到<body>就停止了。上图中为1~3.
  • 处于目标阶段:事件在<div>上发生并处理。但是事件处理会被看成是冒泡阶段的一部分
  • 冒泡阶段:事件又传播回文档。

note:

1)、尽管“DOM2级事件”标准规范明确规定事件捕获阶段不会涉及事件目标,但是在IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两次机会在目标对象上面操作事件。

2)、并非所有的事件都会经过冒泡阶段 。所有的事件都要经过捕获阶段和处于目标阶段,但是有些事件会跳过冒泡阶段:如,获得输入焦点的focus事件和失去输入焦点的blur事件。

两次机会在目标对象上面操作事件例子:

运行效果就是会陆续弹出6个框,为说明原理我整合成了一个图:

3、事件流的典型应用事件代理

传统的事件处理中,需要为每个元素添加事件处理器。js事件代理则是一种简单有效的技巧,通过它可以把事件处理器添加到一个父级元素上,从而避免把事件处理器添加到多个子级元素上。

1、事件代理

事件代理的原理用到的就是事件冒泡和目标元素,把事件处理器添加到父元素,等待子元素事件冒泡,并且父元素能够通过target(IE为srcElement)判断是哪个子元素,从而做相应处理。

传统事件处理,为每个元素添加事件处理器,代码如下:

事件代理的处理方式,代码如下:

<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;"><body>
<ul id="color-list">
<li>red</li>
<li>orange</li>
<li>yellow</li>
<li>green</li>
<li>blue</li>
<li>indigo</li>
<li>purple</li>
</ul>
<script> (function(){ var colorList=document.getElementById("color-list");
    colorList.addEventListener('click',showColor,false); function showColor(e)
    {
        e=e||window.event; var targetElement=e.target||e.srcElement; if(targetElement.nodeName.toLowerCase()==="li"){
        alert(targetElement.innerHTML);
        }
    }
})(); </script>
</body></pre>

2、事件代理的好处

 总结一下事件代理的好处:

  • 将多个事件处理器减少到一个,因为事件处理器要驻留内存,这样就提高了性能。想象如果有一个100行的表格,对比传统的为每个单元格绑定事件处理器的方式和事件代理(即table上添加一个事件处理器),不难得出结论,事件代理确实避免了一些潜在的风险,提高了性能。
  • DOM更新无需重新绑定事件处理器,因为事件代理对不同子元素可采用不同处理方法。如果新增其他子元素(a,span,div等),直接修改事件代理的事件处理函数即可,不需要重新绑定处理器,不需要再次循环遍历。

3、事件代理的问题:【update20170502】

代码如下:事件代理同时绑定了li和span,当点击span的时候,li和span都会冒泡。

<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;"><li><span>li中的span的内容</span></li>

<script> $(document).on('click', 'li', function(e){
        alert('li li');
    });

    $(document).on('click', 'span', function(e){
        alert('li span');
    }) </script></pre>

解决办法:

方法一:span的事件处理程序中阻止冒泡

<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;"> $(document).on('click', 'span', function(e){
        alert('li span');
        e.stopPropagation();
    })</pre>

方法二:li的事件处理程序中检测target元素

<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;"> $(document).on('click', 'li', function (e) { if (e.target.nodeName == 'SPAN') {
            e.stopPropagation(); return;
        }
        alert('li li');
    });</pre>

4、事件代理的一个有趣应用【update20170502】

点击一个列表时,输出对应的索引

<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;"><script>
    var ul=document.querySelector('ul'); var lis=ul.querySelectorAll('ul li');
    ul.addEventListener('click', function (e) { var target= e.target; if(target.nodeName.toUpperCase()==='LI'){
            alert([].indexOf.call(lis,target));
        }
    },false) </script></pre>
查看原文

赞 0 收藏 0 评论 0

Tiger老师 发布了文章 · 4月8日

JS事件简单总结

什么是事件

这个其实不是很好描述,有可能是由用户发起的,比如鼠标事件,键盘事件,窗口事件,表单事件,也有可能是页面加载资源过程中的事件。常见的事件比如:click, dbclick, keydown, keypress, keyup, mousemove, wheel, scroll, focus, blur, load, unload, abort, error, resize, change, select, submit, 大概这些及其相关的。

image.png

事件传播机制

标准的事件是怎么触发以及传播的呢?

总体来说,事件传播分为三个阶段:

1. 事件捕获阶段(capture phase),就是事件从window开始一层一层向目标传递的阶段
2. 目标阶段(target phase),当事件到达事件发生现场的阶段
3. 事件冒泡阶段(bubble phase),和事件捕获阶段相反,事件从目标到window传递的阶段

IE8及其以下的事件只有冒泡,没有捕获。

事件委托/代理

有个提到比较多的就是事件委托。因为事件有冒泡机制,我们可以不在目标元素监听事件,而在它的父元素监听。一个比较经典的例子,一个拥有很多子项的无序列表,需要监听子项上的点击事件。此时,我们将监听绑定在父元素ul上,当点击li元素时,由于事件的冒泡机制,ul层也能触发点击事件。但是如何判断你点击的就是你想要的li元素,而不是ul元素本身,用事件对象event的目标元素属性target来判断就好了,即event.target。

这样做的优点有:提高性能,从监听多个事件减少到监听一个事件,效率肯定得到提升;能动态地自适应,比如再在ul元素内加一个li子元素,传统的方法就需要再增加一个监听事件,而利用事件委托就可以以不变应万变。

事件实现方式

原始事件实现方式(DOM0)

也称DOM0事件处理方式,DOM0不是W3C的标准,由于历史发展的原因,它存在过也一直存在着。很简单,只要在html元素中添加on+事件的属性即可。

<button id='myButton' onclick='doSomething()'> 按钮 </button>

或者用js的方式给元素添加这个属性。

var btEle = document.getElementById('myButton');
btEle.onclick = doSomething; 

这样的优点是简单方便,并且兼容所有的浏览器;缺点也不少,违反了行为与表现分离的准则,只能添加一个事件,也不能利用事件委托机制去更多的事。

IE事件实现方式

上文提到IE8及其以下的事件传播机制只有冒泡,没有捕获,实现方式为

//监听事件
element.attachEvent('on' + eventType, callback);
//解除监听
element.detachEvent('on' + eventType, callback);
//手动触发事件,兼容性IE6-10
element.fireEvent('on' + eventType) 

需要注意的是,匿名函数无法被解除监听。

标准事件实现方式(DOM2)

DOM2规定的标准应该大一统了浏览器,而之后变得不多。目前W3C最新的标准是15年出的DOM4。我们来简单看下。

//监听事件
element.addEventListener(eventType, callback, useCapture);
//解除监听
element.removeEventListener(eventType, callback, useCapture);
//手动触发事件
element.dispatchEvent(eventType)
//回调函数中有个event对象记录事件的属性 
function callback(event){
 //do something
} 

因为标准的事件传播机制有捕获和冒泡阶段,这里最后一个参数useCapture表示是否在事件捕获阶段就执行回调函数,默认为false。一般情况下,我们使用默认值,即在事件冒泡阶段执行函数。为什么?主要是为了兼容旧版的IE。

事件常见属性

我们已经知道,事件的回调函数中有个事件对象的参数。从第一部分,我们已经看到Event对象的继承关系,比如我们最常见的鼠标事件或者键盘事件,MouseEvent继承自UIEvent,UIEvent继承自Event。我们参考最新的W3C规范,来具体看下。

[Constructor(DOMString type, optional EventInit eventInitDict),
Exposed=(Window,Worker)]
interface Event {
 // 事件的类型,比如click,submit,load等
 readonly attribute DOMString type;
 // 触发事件的那个目标元素
 readonly attribute EventTarget? target;
 // 事件传播所在的当前元素
 readonly attribute EventTarget? currentTarget;
 const unsigned short NONE = 0;
 const unsigned short CAPTURING_PHASE = 1;
 const unsigned short AT_TARGET = 2;
 const unsigned short BUBBLING_PHASE = 3;
 // 事件所在的阶段,枚举值如上
 readonly attribute unsigned short eventPhase;
 // 阻止事件的冒泡,在当前元素的冒泡执行完之后
 void stopPropagation();
 // 立即阻止冒泡,包括当前元素还有其他的回调事件
 // DOM3增加新属性 
 void stopImmediatePropagation();
 // 是否在冒泡阶段
 readonly attribute boolean bubbles;
 // 是否可以被取消,用preventDefault取消
 readonly attribute boolean cancelable;
 // 取消当前元素的默认事件,前提是cancelable为true
 void preventDefault();
 // 是否被preventDefault过,DOM3增加新属性 
 readonly attribute boolean defaultPrevented;
 // 事件是否有用户触发的
 [Unforgeable] readonly attribute boolean isTrusted;
 // 当前时间戳
 readonly attribute DOMTimeStamp timeStamp;
 void initEvent(DOMString type, boolean bubbles, boolean cancelable);
};

dictionary EventInit {
 boolean bubbles = false;
 boolean cancelable = false;
}; 

[Constructor(DOMString type, optional MouseEventInit eventInitDict)]
interface MouseEvent : UIEvent {
 // 相对屏幕的x,y坐标
 readonly attribute long screenX; 
 readonly attribute long screenY;
 // 相对浏览器可视区域的x,y坐标
 readonly attribute long clientX;
 readonly attribute long clientY;
 //是否按下这些特殊的键
 readonly attribute boolean ctrlKey;
 readonly attribute boolean shiftKey;
 readonly attribute boolean altKey;
 readonly attribute boolean metaKey;
 //哪个鼠标键被按下,0主键,1滚轮,2附键,只在mouseup,mousedown事件中有效
 readonly attribute short button;
 // 被激活的鼠标,可以是多个,0没有,1主键,2附键,4滚轮,8后退?,16前进?
 // 还可以相加,比如按下了主键和滚轮,就是1+4 = 5
 readonly attribute unsigned short buttons; 
 readonly attribute EventTarget? relatedTarget; 
 boolean getModifierState(DOMString keyArg);
}; 

以上是规范,但实际上浏览器的实现上好像还有很多杂七杂八的属性。以下是在Chrome 56输出的结果。

MouseEvent

感觉眼睛都要花了,比较常用的感觉不多,也看场景吧。一般比如target,preventDefalut,stopPropagation,type,以及坐标的属性比较常用吧。

由于浏览器的历史原因,关于鼠标坐标的属性真是好多对:

属性描述x, y在整个浏览器窗口所处的位置screenX, screenY在整个屏幕中所处于位置clientX, clientY在整个浏览器窗口所处的位置,与x, y属性相同layerX, layerY在整个元素的区域鼠标的位置(但算上了scroll的距离),当不随页面滚动而变化pageX, pageY在整个页面所处的位置,当不随页面滚动而变化offsetX, offsetY在整个元素的区域,鼠标的位置,当不随页面滚动而变化movementX, movementY两个事件之间鼠标的位移差

最好使用W3C规范里面的两对属性,是最安全和最保险的选择,screenX, screenY, clientX, clientY。

简单的兼容实现

主要是考虑对IE8及其以下的兼容性。

function addEventListener(target, event, callback)
 if( window.addEventListener ){
 target.addEventListener(event, callback)
 }
 else if(window.attachEvent){
 target.attachEvent('on' + event, callback);
 }
 else{
 target['on' + event] = callback;
 }
} 

事件的执行顺序

我们用一个简单的例子在Chrome 56中做测试。

当点击parent div的时候输出如下:

parent capture target
parent dom0 js target
parent bubble target 

而点击child div的时候输出如下:

parent capture capture
child dom0 html target
child bubble target
child capture target
parent dom0 js bubble
parent bubble bubble 

当你不断地调整那些事件的顺序,或者引入js的onclick函数,你会发现以下规律:

1. 当html中有onclick事件,js中也有onclick事件的时候,js中onclick事件会覆盖html中的事件。因为其实它们是同一个属性。
2. 目标元素的事件执行顺序跟定义的顺序一致,而不会按照先捕获后冒泡的顺序。在html中定义的事件排在第一个。
3. 非目标事件的函数执行遵守先捕获后冒泡的规律,并且DOM0级元素定义的事件会在冒泡阶段先执行,不管是在html定义还是用js定义。
</pre>

另外,如果一个事件多次绑定,只会执行一次。

事件this指向

对于DOM0级的this情况如下。

而attachEvent中的this指向window,addEventListener指向当前的元素。

自定义事件

事件的机制抽象出来就是发布订阅模式。我们可以利用自定义事件去实现我们自己想要的发布订阅模式。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 5 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-10-15
个人主页被 1.2k 人浏览