最近我也是经历过面试别人和去面试的人了,总结几个常被提及的面试问题,做一下解答和备忘.
JavaScript 中 this 是如何工作的 ?
先来看看这个题目:
this 永远指向函数运行时所在的对象,而不是函数创建时所在的对象
var x = 0;
var foo = {
x: 1,
bar: {
x: 2,
baz: function() {
console.log(this.x)
}
}
}
var a = foo.bar.baz foo.bar.baz() // 2
a() //0
匿名函数和不处于任何对象中的函数,This 指向 window
call, apply, with 指的 This 是谁就是谁.
普通函数调用,函数被谁调用,This 就指向谁
上面的例子中,baz 被 bar 调用所以指向的指 bar. a 运行时所在的对象是 window, 所以指向的是 window.
作用域链?
理解执行环境和上下文
函数调用都有与之相关的作用域和上下文.从根本上说,作用域是基于函数 (function-based) 而上下文是基于对象(object-based).换句话说,作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的.上下文总是关键字 this 的值,是调用当前可执行代码的对象的引用.
执行上下文分有 global,function,eval,一个函数可以产生无数个执行上下文,一系列的执行上下文从逻辑上形成了 执行上下文栈,栈底总是全局上下文,栈顶是当前(活动的)执行上下文.
执行上下文三属性:this 指针,变量对象(数据作用域),作用域链
作用域链 即:一变量在自己的作用域中没有,那么它会寻找父级的,直到最顶层.过程如下:
任何在执行上下文时刻的作用域都由作用域链来实现
在一个函数被定义的时候, 会将它定义时刻的 scope chain 链接到这个函数对象的 [[scope]] 属性
在一个函数对象被调用的时候,会创建一个活动对象 (也就是一个对象), 然后对于每一个函数的形参,都命名为该活动对象的命名属性, 然后将这个活动对象做为此时的作用域链(scope chain) 最前端, 并将这个函数对象的 [[scope]] 加入到 scope chain 中.
上面的文字大家可以好好琢磨一下,可以更好的理解函数作用域.
函数声明提升和变量声明提升 (Hoisting) ?
我们先来了解 js 编译器在执行代码的过程:
以执行一段 function 代码为例:
第一步:创建可执行上下文(以下简称为 EC),压入当前的 EC 栈中.EC 中包括了以下信息:
词法环境(= 环境记录项(保存变量,函数声明和形参)+ 外部词法环境(function 的 [[scope]] 属性,作用域链的本质))
this 的指针
变量环境(与环境记录项的值相同,但不再发生变动.)
第二步:收集函数声明,变量声明和形参,保存在环境记录项内.这个收集的过程,就是一般所谓的声明提升现象的本质.如果发现了重复的标识符,则优先级为函数声明 ,形参 ,变量声明(优先级低的会被无视).
第三步:开始执行代码,环境记录项内没有的标识符会根据作用域链查找标识符对应的值,环境记录项亦有可能因赋值语句而被修改.
第四步:函数执行完毕,EC 栈被弹出,销毁.
好了,第二步说的很清楚了 声明提升 (Hoisting) 现象就是在收集函数,变量声明和形参的过程会根据函数声明,形参,变量声明的顺序优先级来收集.
例子:
理解了吗?
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a);
// 输出1 由于函数声明提升,b内的实际是这样:
// function b() {
// function a() {}; 这里是函数声明提升
// a = 10;
// return;
// function a() {}
// }
勘误:谢谢 github 上有同学的指正 关于博客中的一个问题 · Issue #1 · stephenzhao/hexo-theme-damon ,上面的正确执行应该为先进行预编译,所以先执行 function a(){},然后会进行对 a 的赋值操作.
什么是闭包,如何使用它,为什么要使用它?
//正确的顺序应该为:
// function b() {
// function a() {}
// a = 10;
// return;
// }
还是上面的题目,做个变形.
上面的例子中 ba'r 里面返回了一个匿名函数,这个匿名函数可以在外部被调用即:foo.bar()() 读取到了 bar 的执行上下文的变量对象 that,这个函数就形成了一个闭包.
var x = 0;
var foo = {
x: 1,
bar: function() {
console.log(this.x);
var that = this;
return function() {
console.log(this.x) console.log(that.x)
}
}
}
foo.bar() // 1
foo.bar()() // this: 0, that: 1
好了,我们理解了上面的套路,下面来解释闭包就好理解了.
闭包就是能够读取其它函数内部变量的函数
在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 "定义在一个函数内部的函数"
用途:
var x = 0;
var bar:function () {
var n = 999;
return function () {
return n;
}
}
var outer = bar();
outer() // 999
读取函数内部的变量
让这些变量的值始终保持在内存中
我们修改一下上面的代码
说明,n 一直保存在内存当中,而没有在 bar() 执行完成之后被销毁;
var add;
var bar = function() {
var n = 999;
add = function() {
n += 1;
}
return function() {
return n;
}
}
var outer = bar();
outer() // 999
add();
outer(); // 1000
原因:
bar 里面的匿名函数被赋值给了 outer,这个导致在 outer 没有被销毁的时候,该匿名函数一直存在内存当中,而匿名函数的存在依赖于 bar,所以 bar 需要使用都在内存当中,所以 bar 并不会在调用结束后呗垃圾回收机制给收回.
而上面的 add 接受的也是一个匿名函数,该匿名函数本身也是闭包,所以也可以在外部操作里面的变量.
注意点
会导致内存泄漏,慎用
闭包会修改内部变量的值,所以在使用闭包作为对象的公用方法时要谨慎.
闭包的一个应用,单例模式
单例模式的定义是产生一个类的唯一实例
单例模式在 js 中经常会遇到,比如 var a = {}; 其实就是一个单例子.
但是我们写一个更有意义的单例:
更简洁一点的:
var singleton = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
}
又是半夜,这两天在看里约奥运会的比赛,林丹和李宗伟的那场比赛是今年看过的经次于 nba 总决赛最后一场的精彩程度.一个伟大的英雄,需要另一个伟大的对手来成就,感谢林丹,感谢李宗伟世界会记住你们.晚安.
var singleton = (function() {
var instance;
return function(object) {
if (!instance) {
instance = new object();
}
return instance;
}
})();
接下来的文章讲解一些关于 js 面向对象 的东西,敬请关注我的专栏 《前端杂货铺》
来源: https://juejin.im/post/5a5461ec6fb9a01cba4271bc