JS面试真题 part3
- 11、bind、call、apply区别?如何实现一个bind
- 12、JavaScript中执行上下文和执行栈是什么
- 13、说说JavaScript中的事件模型
- 14、解释下什么是事件代理?应用场景?
- 15、说说你对闭包的理解?闭包使用场景
11、bind、call、apply区别?如何实现一个bind
自己回答:
相同点:都是改变函数的this指向,函数接收的第一个参数都是要指向的对象。
不同点:函数有没有执行以及参数传递的方式不同
bind函数并没有执行。
call和apply函数执行了。
call的参数第一个是要指向的对象,后面是参数列表
apply的参数第一个是要指向的对象,第二个是数组,数组里是参数列表
bind的参数第一个是要指向的对象,后面是参数列表,可以分次传。
bind实现:
Function.prototype.bindFunc=function(obj,...agament){
let that=this
return function(...agament2){
let agamentAll=[...agament,...agament2]
let func=that.apply(obj,agamentAll)
return func
}
}
验证:
function getName(age,sex){
console.log(this.name,age,sex)
return {
name:this.name,
age:age,
sex:sex
}
}
let obj={
name:'张三'
}
let newFunc=getName.bindFunc(obj,'18')
let aa =newFunc('女')
console.log('aa',aa)
标准回答:
call
、apply
、bind
作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this
指向
- 三者都可以改变函数的
this
对象指向 - 三者第一个参数都是
this
要指向的对象,如果没有这个参数或参数为undefined
或null
,则默认指向全局window
- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分多次传入 bind
是返回绑定this
之后的函数,apply
、call
则是立即执行
实现:
分为三步
- 修改
this
指向 - 动态传递参数
- 兼容
new
关键字
自己回答实现里的少了兼容new关键字,如果我们对bind返回的函数使用new会发生什么呢
function Person(name, age) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.bind(null, '前端西瓜哥');
const boundPerson = new BoundPerson(100);
// Person {name: '前端西瓜哥', age: 100}
boundPerson.__proto__ === Person.prototype
// true
等价于直接new原来的Person函数,依然可以进行参数分次传递
修改后
Function.prototype.bindFunc=function(obj,...agament){
let that=this
return function Fn(...agament2){
let agamentAll=[...agament,...agament2]
let func=this instanceof Fn?new that(...agamentAll):that.apply(obj,agamentAll);
return func
}
}
12、JavaScript中执行上下文和执行栈是什么
自己回答:
JavaScript中执行上下文:当前变量或函数的生效范围和集合
执行栈:全局执行上下文和函数执行上下文的执行顺序
标准回答:
定义:执行上下文是一种对js代码执行环境的抽象概念,只要有JavaScript代码运行,那么它就一定是运行在执行上下文中。
执行上下文包含了三个重要的组成部分:变量对象、作用域链和this值。
执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器的全局对象就是
window
对象,this
指向这个全局对象 - 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- Eval函数执行上下文:指的是运行在eval函数中的代码,很少用而且不建议使用
执行上下文的生命周期包括三个阶段:创建阶段->执行阶段->回收阶段
创建阶段
- 生成变量对象
- 创建arguments:如果是函数上下文,首先会创建
arguments
对象,给变量对象添加形参名称和值。 - 扫描函数声明:对于找到的函数声明,将函数名和函数引用(指针)存入
VO
中,如果VO
中已经有同名函数,那么就进行覆盖(重写引用指针)。 - 扫描变量声明:对于找到的每个
变量声明
,将变量名存入VO
中,并且将变量的值初始化为undefined
。如果变量的名字已经在变量对象里存在,不会进行任何操作并继续扫描。
- 创建arguments:如果是函数上下文,首先会创建
- 建立作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。
- 确定this的指向:如果当前函数被作为
对象方法调用
或使用 bind、call、apply 等 API
进行委托调用,则将当前代码块的调用者信息
(this value)存入当前执行上下文,否则默认为全局对象调用
。
let和const定义的变量在创建阶段没有被赋值,var声明的变量在创建阶段被赋值为undefined。创建阶段,扫描函数声明和变量,然后将函数声明存储在环境中,变量初始化为undefined(var声明时)
函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。
执行阶段
执行阶段 中,执行流进入函数并且在上下文中运行/解释代码,JS引擎开始对定义的变量赋值、开始顺着作用域链访问变量、如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出
- 变量赋值:如果找不到变量的值,将会为其分配undefined
- 函数的引用
- 执行其他代码
回收阶段
- 执行上下文等待虚拟机回收执行上下文
执行上下文栈:
执行栈,也叫调用栈,具有LIFO(后进先出)结构,用于存储在代码执行期间创建的所有上下文。
顺序如下:
- 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
- 在函数执行上下文创建后, 将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后, 栈中只剩下window
举例:
13、说说JavaScript中的事件模型
自己回答:
事件模型?
忘了概念,复习浏览器事件详解
标准回答:
事件模型之前先阐述一下,事件与事件流
1、事件与事件流
事件:在html文档或者浏览器中发生的一种交互操作,使得网页具有互动性,常见的有加载事件、鼠标事件、自定义事件等。
由于dom是数结构,父子节点都绑定事件,存在一个顺序问题,这就涉及到了事件流
事件流都会经历三个阶段:
-事件捕获阶段:从上往下传递
- 处于目标阶段
- 事件冒泡阶段:从下往上传播
事件模型包括: - 原始事件模型(DOM0级)
- 标准事件模型(DOM2级)
- IE事件模型(基本不用)
原始事件模型:
DOM0级事件具有很有的跨游览器优势,会以最快的速度绑定,但是由于绑定速度太快,可能页面还没加载完全
- 只支持冒泡,不支持捕获
- 同一种类型的事件只能绑定一次
标准事件模型:
一共三个阶段:事件捕获、事件处理、事件冒泡
事件绑定监听函数如下:
addEventListener(eventTpye,hander,useCapture)
事件移除监听函数如下:
removeEventListener(eventTpye,hander,useCapture)
- eventTpye指定事件类型(不要加on)
- hander是事件处理函数
- useCapture是一个
boolean
用于指定是否在捕获阶段进行处理,一般设置为false
与ie浏览器保持一致
特性:
- 可以在一个DOM元素上绑定多个事件处理器,各自并不会冲突
- 执行时机,与useCapture设置有关
IE事件模型:
一共俩个阶段:事件处理、事件冒泡(没有事件捕获)
事件绑定监听函数如下:
attachEvent(eventTpye,hander)
事件移除监听函数如下:
attachEvent(eventTpye,hander)
14、解释下什么是事件代理?应用场景?
自己回答:
事件代理,由父级绑定事件,通过子节点id触发对应子节点的事件,避免多次绑定事件,也避免新增的子节点漏掉绑定事件
应用场景:li节点事件监听,在父级ul或div上绑定监听事件
标准回答:
事件代理,就是把一个元素响应事件(click、keydown…)的函数委托到另一个元素
事件流会经过三个阶段:捕获->目标->冒泡阶段,事件委托就是在冒泡阶段完成
事件委托,会把一个或者一组元素的事件委托到它的父层或者更外的元素上,真正绑定事件的是外层元素,而不是目标元素
当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
应用场景:
如果有个列表,列表中有大量的列表项,点击列表项响应一个事件。如果给每个列表项都绑定一个函数的话,那么对内存的消耗是非常大的,如果用户还能动态增加或删除列表元素,那每次改变的时候都需要给增加的元素绑定事件,给删除的元素解除事件。这时候就可以事件委托,把事件绑定在父级元素ul
上面,然后执行事件时候再匹配目标元素,和目标元素的增减也没有关系。
如:
<input type="button" name="" id="btn" value=" " />
<ul id="ul1">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item 4</li>
</ul>
使用事件委托
const oBtn = document.getElementById("btn");
const oUl = document.getElementById("ul1");
const num = 4;
//
oUl.onclick = function (ev) {
ev = ev || window.event;
const target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == 'li') {
console.log('the content is: ', target.innerHTML);
}
};
//
oBtn.onclick = function () {
num++;
const oLi = document.createElement('li');
oLi.innerHTML = `item ${num}`;
oUl.appendChild(oLi);
};
总结:
适合事件委托的事件有:click、mousedown、mouseup、keydown、keyup、keypress
15、说说你对闭包的理解?闭包使用场景
自己回答:
函数内部访问外部的变量,形成闭包
使用场景:var定义变量的for循环,内部使用异步,如定时器内访问变量,用闭包进行传递,可以 让for循环的变量传参正确
标准回答:
一个函数和对其周围状态的引用绑定在一起,这样的组合就是闭包
闭包让你可以在一个内层函数中访问到其外层函数的作用域
使用场景:
任何闭包的使用场景都离不开这两点:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
1、在页面上添加一些可以调整字号的按钮
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
2、柯里化函数
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能轻松的重用
//
function getArea(width, height) {
return width * height
}
// 10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
//
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 10
const area1 = getTenWidthArea(20)
//
const getTwentyWidthArea = getArea(20)
3、用闭包模拟私有方法
在javaScript
中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上述通过闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式
两个计数器 Counter1
和Counter2
是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包的变量
4、例如计数器、延迟调用、回调等闭包的应用,核心思想是创建私有变量和延长变量的生命周期
注意事项
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。
原因在于每个对象的创建。方法都会被重新赋值
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包,修改如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};