【前端面试】作用域和闭包
1. 题目
说一下对变量提升的理解
说明this的几种不同使用场景
创建10个a标签,点击的时候弹出来相应的序号
如何理解作用域
实际开发中闭包的应用
手动实现call apply bind
2. 知识点
2.1 执行上下文
范围:一段script或者一个函数
全局:变量定义、函数声明 script
函数:变量定义、函数声明、this、arguments (执行之前)
函数声明和函数表达式的区别:
a(); //报错 函数表达式 变量声明 会提前。 var a = function(){} b(); // 不报错 函数声明 function b(){}
变量定义时会默认把他的变量声明提升:(仅限于他的执行上下文,比如一段script和一个函数中)
console.log(a); var a = 0;
实际上是
var a; console.log(a); a = 0;
2.2 this
this要在执行时才能确认,定义时无法确认。
var a = { name:'a', fn:function(){ console.log(this.name); } } a.fn(); // a a.fn.apply({name:'b'}); // b a.fn.call({name:'b'}); var fn1 = a.fn(); fn1(); // undefined
this的使用场景
构造函数中(指向构造的对象)
function Fun(name){ this.name = name; } var f = new Fun('a'); console.log(f.name);
对象属性中(指向该对象)
普通函数中(指向window)
call apply bind
都是用来改变一个函数的this指向,用法略有不同。
call:后面的参数为调用函数的参数列表
function greet(name) { console.log(this.animal,name); } var obj = { animal: 'cats' }; greet.call(obj,'猫咪');
apply:第二个参数为调用函数的参数数组
function greet(name) { console.log(this.animal,name); } var obj = { animal: 'cats' }; greet.apply(obj,['猫咪']);
bind:当绑定函数被调用时,bind传入的参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。
var fun = function (name1,name2){ console.log(this); console.log(name); }.bind({a:1},"name1"); fun("name2");
arguments中的this:
var length = 10; function fn(){ alert(this.length) } var obj = { length: 5, method: function(fn) { arguments[0]() } }
obj.method(fn)//输出1
这里没有输出5,也没有输出10,反而输出了1,有趣。这里arguments是javascript的一个内置对象(可以参见mdn:arguments - JavaScript),是一个类数组(就是长的比较像数组,但是欠缺一些数组的方法,可以用slice.call转换,具体参见上面的链接),其存储的是函数的参数。也就是说,这里arguments[0]指代的就是你method函数的第一个参数:fn,所以arguments[0]()的意思就是:fn()。
不过这里有个疑问,为何这里没有输出5呢?我method里面用this,不应该指向obj么,至少也会输出10呀,这个1是闹哪样?
实际上,这个1就是arguments.length,也就是本函数参数的个数。为啥这里的this指向了arguments呢?因为在Javascript里,数组只不过使用数字做属性名的方法,也就是说:arguments[0]()的意思,和arguments.0()的意思差不多(当然这么写是不允许的),你更可以这么理解:
arguments = { 0: fn, //也就是 functon() {alert(this.length)} 1: 第二个参数, //没有 2: 第三个参数, //没有 ..., length: 1 //只有一个参数 }
所以这里alert出来的结果是1。
如果要输出5应该咋写呢?直接 method: fn 就行了。
2.3 作用域
没有块级作用域
if(true){ var name = "test" } console.log(name);
尽量不要在块中声明变量。
只有函数级作用域
2.4 作用域链
自由变量 当前作用域没有定义的变量 即为自由变量。
自由变量会去其父级作用域找。是定义时的父级作用域,而不是执行。
var a = 100; function f1(){ var b = 200; function f2(){ var c = 300; console.log(a); //自由变量 console.log(b); //自由变量 console.log(c); } f2(); }; f1();
2.5 闭包
一个函数中嵌套另外一个函数,并且将这个函数return出去,然后将这个return出来的函数保存到了一个变量中,那么就创建了一个闭包。
闭包的两个使用场景
1.函数作为返回值
function fun(){ var a = 0; return function(){ console.log(a); //自由变量,去定义时的父级作用域找 } } var f1 = fun(); a = 1000; f1();
2.函数作为参数
function fun(){ var a = 0; return function(){ console.log(a); //自由变量,去定义时的父级作用域找 } } function fun2(f2){ a = 10000 f2(); } var f1 = fun(); fun2(f1);
具体解释看 高级-闭包中的说明
闭包的两个作用:
能够读取其他函数内部变量的函数
可以让函数内部的变量一直保存在内存中
实际应用场景1:
闭包可以将一些不希望暴露在全局的变量封装成“私有变量”。
假如有一个计算乘积的函数,mult函数接收一些number类型的参数,并返回乘积结果。为了提高函数性能,我们增加缓存机制,将之前计算过的结果缓存起来,下次遇到同样的参数,就可以直接返回结果,而不需要参与运算。这里,存放缓存结果的变量不需要暴露给外界,并且需要在函数运行结束后,仍然保存,所以可以采用闭包。
上代码:
function calculate(param){ var cache = {}; return function(){ if(!cache.parame){ return cache.param; }else{ //缓存计算.... //cache.param = result //下次访问直接取 } } }
实际应用场景2
延续局部变量的寿命
img 对象经常用于进行数据上报,如下所示:
var report = function( src ){ var img = new Image(); img.src = src; }; report( 'http://xxx.com/getUserInfo' );
但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在 bug,在这些浏览器
下使用 report 函数进行数据上报会丢失 30%左右的数据,也就是说, report 函数并不是每一次
都成功发起了 HTTP 请求。
丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的
调用结束后, img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求
就会丢失掉。
现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:
var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; } })();
闭包缺点:浪费资源!
3. 题目解答
3.1 说一下对变量提升的理解
变量定义和函数声明
注意函数声明和函数表达式的区别
变量定义时会默认把他的变量声明提升:(仅限于他的执行上下文,比如一段script和一个函数中)
console.log(a); var a = 0;
实际上是
var a; console.log(a); a = 0;
3.2 说明this的几种不同使用场景
- 构造函数中(指向构造的对象)
- 对象属性中(指向该对象)
- 普通函数中(指向window)
- call apply bind
3.3 创建10个a标签,点击的时候弹出来相应的序号
实现方法1:用let声明i
var body = document.body; console.log(body); for (let i = 0; i < 10; i++) { let obj = document.createElement('i'); obj.innerHTML = i + '<br>'; body.appendChild(obj); obj.addEventListener('click',function(){ alert(i); }) }
实现方法2 包装作用域
var body = document.body; console.log(body); for (var i = 0; i < 10; i++) { (function (i) { var obj = document.createElement('i'); obj.innerHTML = i + '<br>'; body.appendChild(obj); obj.addEventListener('click', function () { alert(i); }) })(i) }
3.4 实际开发中闭包的应用
能够读取其他函数内部变量的函数
可以让函数内部的变量一直保存在内存中
封装变量,权限收敛
应用1
var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; } })();
用于防止变量销毁。
应用2
function isFirstLoad() { var arr = []; return function (str) { if (arr.indexOf(str) >= 0) { console.log(false); } else { arr.push(str); console.log(true); } } } var fun = isFirstLoad(); fun(10); fun(10);
将arr封装在函数内部,禁止随意修改,防止变量销毁。
3.5 手动实现call apply bind
- context 为可选参数,如果不传的话默认上下文为 window;
- context 创建一个 Symbol 属性,调用后即删除,不会影响context
Function.prototype.myCall = function (context) { if (typeof this !== 'function') { return undefined; // 用于防止 Function.prototype.myCall() 直接调用 } context = context || window; const fn = Symbol(); context[fn] = this; const args = [...arguments].slice(1); const result = context[fn](...args); delete context[fn]; return result; }
apply实现类似call,参数为数组
Function.prototype.myApply = function (context) { if (typeof this !== 'function') { return undefined; // 用于防止 Function.prototype.myCall() 直接调用 } context = context || window; const fn = Symbol(); context[fn] = this; let result; if (arguments[1] instanceof Array) { result = context[fn](...arguments[1]); } else { result = context[fn](); } delete context[fn]; return result; }
1.判断是否为构造函数调用
2.注意参数要插入到目标函数的开始位置
Function.prototype.myBind = function (context) { if (typeof this !== 'function') { throw new TypeError('Error') } const _this = this const args = [...arguments].slice(1) return function F() { // 判断是否用于构造函数 if (this instanceof F) { return new _this(...args, ...arguments) } return _this.apply(context, args.concat(...arguments)) } }