1. 说说前端中的事件流
HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。
什么是事件流:事件流
描述的是从页面中接收事件的顺序.
DOM2级事件流包括下面几个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段
addEventListener:addEventListener 是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。默认是false
IE只支持事件冒泡。
2. 如何让事件先冒泡后捕获
在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获事件。
什么是事件冒泡
事件冒泡:开始时由最具体的元素接收,然后逐级向上传播到到 DOM 最顶层节点。
js怎么阻止事件冒泡
1.标准写法:利用事件对象里面的stopPropagation()方法
var div = document.querySelector('div')
div.addEventListener('click', function(e) {
e.stopPropagation()
})
2.非标准写法:IE6-8 利用事件对象cancelBubble属性
var div = document.querySelector('div')
div.addEventListener('click', function(e) {
e.cancelBubble = true // cancel 取消 Bubble 泡泡
})
3.说一下事件委托(事件代理)
简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。
举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。(减少内存消耗、动态绑定事件。)
什么是事件监听
addEventListener()方法,用于向指定元素添加事件句柄,它可以更简单的控制事件,语法为
element.addEventListener(event, function, useCapture);
-
第一个参数是事件的
类型
(如 “click” 或 “mousedown”) -
第二个参数是事件触发后调用的
函数
-
第三个参数是个布尔值用于描述事件是
冒泡还是捕获
。该参数是可选的。事件传递有两种方式,冒泡和捕获,它定义了元素事件触发的顺序
如果将P元素插入到div元素中,用户点击P元素,在冒泡中,内部元素先被触发,然后再触发外部元素;在捕获中,外部元素先被触发,再触发内部元素
事件代理在捕获阶段的实际应用
可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。
4.1 执行环境、作用域链、环境栈
1. 执行环境(又称执行上下文、作用域、环境)
执行环境:
是js执行一段代码时的运行环境。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象
,环境中定义的所有变量和函数都保存在这个对象中。【该对象保存了变量提升的内容】
函数执行环境:
每个函数都有自己的执行环境。当执行流进入一个函数时(即调用该函数),函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
全局执行环境:
是最外围的一个执行环境。全局执行环境被认为是window对象。
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出 – 例如关闭网页或浏览器时,才会被销毁。)
作用域:就是在程序中定义变量和函数的可见范围,即作用域控制着变量和函数的可见性和生命周期。【大白话】
2. 作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一致延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
js经典面试题–变量提升、执行环境、作用域链
3.环境栈(又称调用栈)
1.在执行环境创建好后,JavaScript 引擎会将执行环境压入栈中,通常把这种用来管理执行环境的栈称为环境栈,又称调用栈。调用栈是 JavaScript 引擎追踪函数执行的一个机制。
2.调用栈是一种用来管理执行环境的数据结构,符合后进先出的规则
4.2 词法作用域、动态作用域(this)
1.词法作用域(静态的作用域)
词法作用域就是指作用域是由函数声明的位置来决定的,它是在代码编译阶段就决定好的,和函数是怎么调用的没有关系。(肉眼可见的函数外部的位置
)
JavaScript词法作用域
2.动态作用域(与this的调用有关)
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用
。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
5.1 改变函数内部this指针的指向函数(bind,apply,call的区别)
相同点:
- 改变 this 指向
不同点:
- call 和 apply 是立刻执行的,而 bind 是返回了一个新的函数,这个函数不会马上执行,需要用
()
调用,且返回的函数还可以传入新的参数——支持柯里化形式传参 fn(1)(2) - call 和 apply 的第一个参数都是:要改变指向的对象。但第二个参数不同,call 可以传递多个参数,是arg1, arg2……这种形式;而 apply 是数组
实现 call
面试 |call, apply, bind的模拟实现和经典面试题
Function.prototype.MyCall = function (context, ...args) {
// this 指的是 fn (是 fn 调用的 MyCall)
console.log(this);
// context 就是 传入的obj
// 如果 context 为空,默认为 window (参考call的处理方式)
context = context || window;
// 在context上扩展一个 fn 方法 (将函数挂载到 context 上)
context.fn = this;
// 调用 context.fn,此时fn的 this 指向 contetx
context.fn(...args);
// 删除对象上的属性 (不然 fn 这个方法就会留在调用的对象上)
delete context.fn;
return
};
let name = "张三";
let obj = {
name: '李四'
};
function fn() {
console.log(this.name, ...arguments);
}
fn.MyCall(null);
fn.MyCall(obj, 11, 22);
// fn.call(null);
// fn.call(obj, 11, 22);
console.log(obj); // obj 上没有挂载 fn (已经delete了)
实现 apply
Function.prototype.MyApply = function (context, args) {
context = context || window;
context.fn = this;
if(args) {
context.fn(...args);
} else {
context.fn();
}
delete context.fn;
return
};
let name = "张三";
let obj = {
name: '李四'
};
function fn() {
console.log(this.name, ...arguments);
}
fn.MyApply(null);
fn.MyApply(obj,[11,22]);
// fn.apply(null);
// fn.apply(obj,[11,22]);
实现 bind
- 函数调用,改变 this
- 返回一个绑定 this 的函数
- 接收多个参数
- 支持柯里化形式传参 fn(1)(2)
Function.prototype.MyBind = function (context, ...args) {
// 获取调用 MyBind 的函数主体 (fn)
const _this = this;
// 返回一个函数
// 因为支持柯里化形式传参,我们需要再次获取存储参数 (即第二次传入的参数)
return (...otherArgs) => {
_this.call(context, ...args.concat(...otherArgs))
}
};
var obj = {
name: '张三'
};
function fn() {
console.log(this.name, ...arguments)
}
// 使用 MyBind 调用
fn.MyBind(obj, 12, 23, 34, 56)(88, 99);
var a = fn.MyBind(obj, 12, 23, 34, 56);
a();
// // 使用原生 bind 调用
// fn.MyBind(obj, 12, 23, 34, 56)(88, 99);
//
// var aa = fn.bind(obj, 12, 23, 34, 56);
// aa();
5.2 this的指向问题
你不知道的js中关于this绑定机制的解析[看完还不懂算我输]
四种绑定规则的优先级:显式绑定和new绑定无法直接比较(会报错)
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定
this指的是当前作用域的实例对象
- 默认绑定: 在非严格模式下,默认绑定的 this 指向
window
,严格模式下 this 指向undefined
- 隐式绑定: 函数在调用位置,是否有上下文对象,如果有,那么 this 就会隐式绑定到这个
对象
上 - 显示绑定: 在某个对象上强制调用函数,从而将 this 绑定在这个
对象
上,我们可以通过apply/call/bind
将函数中的 this 绑定到指定对象上 - new绑定: 使用构造函数调用时,this 会自动绑定在 new 创建的对象上
- 箭头函数:
继承
最近一层非箭头函数的 this(箭头函数的 this 一旦绑定了上下文,就不会被任何代码改变)
5.3 this 的设计缺陷以及应对方案
1. 缺陷:嵌套函数中的 this 不会从外层函数中继承
2. 解决方案:
- 把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
- 继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。
5.4 箭头函数与普通函数的区别
普通函数:
通过 function 关键字定义,this 指向取决于函数的调用方式
箭头函数:
1.使用" => "定义,语法更加简洁、清晰
2.箭头函数没有 prototype (原型),所以箭头函数本身没有this
箭头函数中的this是根据外层作用域来决定的,继承外层函数调用的this绑定
// 箭头函数
let a = () => {};
console.log(a.prototype); // undefined
// 普通函数
function a() {};
console.log(a.prototype); // {constructor:f}
3.箭头函数不会创建自己的this
箭头函数没有自己的this,箭头函数的this指向在定义(注意:是定义时,不是调用时)的时候继承自外层第一个普通函数的this。所以,箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变。
4.call | apply | bind 无法改变箭头函数中this的指向
call | apply | bind方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向。
5.箭头函数不能作为构造函数使用
6.箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表
6.js的new操作符做了哪些事情
1.用new关键字新建了一个空对象
2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3.在构造函数内使用this为新对象添加属性
4.如果函数没有返回其他对象,那么 new 表达式中的函数调用会返回这个新对象
实现new
// fn:构造函数
function _new(fn, ...arg) {
// 创建一个新对象,使用现有的对象作为 新创建的对象的原型 (__proto__)
const newObj = Object.create(fn.prototype);
// 构造函数内部的 this 被赋值给这个新对象 (即 this 指向新对象)
const obj = fn.apply(newObj, arg);
// 如果构造函数返回一个对象,则返回该对象;否则,返回刚创建的新对象
return obj instanceof Object ? obj : newObj;
}
function Person(name, age) {
this.name = name;
this.age = age;
this.speak = function () {
console.log('my name is ' + this.name)
};
// 当构造函数 return 了一个对象时, 返回该对象
// return {
// content: '333333'
// }
// 当构造函数 return null 时, 返回新对象(newObj)
// 因为 null instanceof Object === false
// return null
}
const Amy = _new(Person,"Amy", 19);
console.log(Amy); // Person {name: "Amy", age: 19, speak: ƒ}
【1】构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
【2】如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用