基础程度:4颗星
知识点
主要涉及是预编译变量提升、AO
GO、new的使用,还有静态方法,实例方法之类的基础知识
牛客网2021凡科春招前端(大佬面经)
一、题目
function fn() {
getName = function () {
console.log('yifang')
}
return this;
}
fn.getName = function () {
console.log('liudehua')
}
fn.prototype.getName = function () {
console.log('zhangxueyou')
}
var getName = function () {
console.log('zhouxingci')
}
function getName() {
console.log('huangzesi')
}
// 依次以下代码分别输出什么
fn().getName();
fn.getName();
getName();
new fn.getName();
new new fn.getName();
二、分析
简单入手
先不着急看答案,也别看全部的题,先看第一个,一般一开始的不会太难,稳住心态
- fn().getName();输出“yifang”,为什么呢?因为其实语句的意思是执行fn,然后执行fn中的getName方法;它返回的是this,详细看下面拓展1,拓展2,拓展3会更好理解,主要考的是变量提升,预编译的问题;
- fn.getName(); 输出“liudehua”,因为这是fn对象的一个静态方法(函数也是对象),这也没理由是其他答案,不理解的可以看看实例方式与静态方法的区别;
- getName(); 蛮复杂的下一小节分析;
- new fn.getName(); 输出“liudehua”,为什么不是“zhangxueyou”呢?new它并没有实例化fn,所以其实这条语句相当于刚刚的fn.getName();(斜体字指的是猜想的),有时间可以看看new一个过程到底发生了什么?
- new new fn.getName(); 输出“liudehua”,这题其实会报错,可能那个提供面经的大佬记错题了
复杂语句简单分析
为啥说getName()蛮复杂的呢?因为这个涉及到了变量提升,作用域AO,GO这些,所以我就简单说说这个题中为什么会输出“yifang”
function fn() {
getName = function () { ... }
return this;
}
// fn.getName = function () { ... }
// fn.prototype.getName = function () {}
var getName = function () { ... }
function getName() { ... }
// 依次以下代码分别输出什么
fn().getName();
fn.getName();
getName();
...
-
无关的代码都删掉或注释了,答案输出“yifang”是有前提的
-
先看看GO对象(不懂GO,预编译的可以看看总结),步骤如下
- 寻找变量声明:有了,是getName
- 寻找函数声明,并赋值:没有,因为刚刚已经有个叫getName了,所以直接赋值,值为函数体
-
来执行代码咯!
-
fn().getName(); fn函数执行,getName又被赋值,输出”yifang“;
-
fn.getName(); 这行无影响,执行fn的一个静态方法
-
getName(); 执行,输出“yifang”,就是那么简单!
-
想要更加理解看拓展1,拓展2,拓展3
三、拓展
一
这里我们把输出改成:
... // 前面都不变
getName(); // "zhouxingci"
fn().getName(); // "yifang"
getName(); // "yifang"
分析:为什么同样执行getName,两次打印的答案是不一样呢,其实刚刚上面的分析就写了,由于第二句的fn()给getName赋值了,如果没有执行那个fn().getName(),结果是:
- GO对象跟刚刚一样,此时getName的值是function getName() { console.log(‘huangzesi’) };
- 好了,开始执行代码,遇到getName的赋值语句, getName被赋值为function () { console.log(‘zhouxingci’) },所以第一次打印的结果是"zhouxingci"。
二
其实前面分析并没去分析到fn函数的AO,现在我们又来改改题目:
function fn() {
/*- getName = function () { console.log('yifang') } */
var getName = function () { console.log('yifang') }
return this;
}
// 无关省略
var getName = function () { console.log('zhouxingci') }
function getName() { console.log('huangzesi') }
// 依次以下代码分别输出什么
getName();
fn().getName();
getName();
我们在fn函数里面重新声明一个变量叫getName,那么我们打印会发生什么改呢?
- 首先先来看看第一次执行getName,他还是输出"zhouxingci",因为没有执行fn(),所以改了代码其实影响不大的,可以重新看看前面的分析
- 第二行:fn().getName();输出“zhouxingci”,为啥子输出的不是"yifang"呢?因为fn函数返回的是this,这里的执行环境this指的是window对象,其实就是GO。而这个同名的变量getName是属于fn的AO里面的,所以不会打印出“yifang”。此时等价于,fn().getName() === this.getName() —> getName() (仅仅是建立此环境上的结论)
- 又来看看第二次执行的getName,整个变量提升(预编译)过程分析一下:
- GO对象跟刚刚一样,此时getName的值是function getName() { console.log(‘huangzesi’) };
- 分析函数fn的AO,先找形参和变量声明,发现没有形参,只声明了一个变量getName,值为undefined
- 来执行getName(),getName被赋值为function () { console.log(‘zhouxingci’) },所以输出“zhouxingci”
- 来执行fn().getName(),fn的AO中的getName被赋值function () { console.log(‘yifang’) },然后输出了”zhouxingci“,原因前面那点说了,此时this == window, window == GO,GO中getName值此时为function () { console.log(‘zhouxingci’) }。
所以其实还是输出"zhouxingci";
三
与拓展4一样,我们来看看在fn函数内部,手动给this.getName赋值:
function fn() {
var getName = function () { console.log('yifang') }
this.getName = getName;
return this;
}
// 无关省略
var getName = function () { console.log('zhouxingci') }
function getName() { console.log('huangzesi') }
// 依次以下代码分别输出什么(给出了答案)
getName(); // "zhouxingci"
fn().getName(); // "yifang"
getName(); // "yifang"
手动给this即window(当前环境this为window)中的getName赋值为函数里面同名getName的值后,fn().getName()输出变成了”yifang“,getName()也是”yifang“,该结果与拓展1一样;
答案
fn().getName(); // yifang
fn.getName(); // liudehua
getName(); // yifang
new fn.getName(); // liudehua
new new fn.getName(); // liudehua
总结
预编译其实就是程序执行前的一个步骤;比如var a = 1;这里有两个步骤,一个是声明变量,一个是赋值。那么GO又是什么呢?GO是global Oject(全局对象),AO是活动对象,也称函数上下文,函数们在执行前一刻会生成自己的AO
- GO对象
- 寻找变量声明,并赋值为undefined
- 寻找函数声明,并赋值为该函数体
- 等待执行代码
引申一下,还有个暗示全局变量,即变量未经声明赋值,此变量就为全局对象所有;
- AO对象
- 寻找形参和变量声明
- 实参形参对应,就是给形参赋值,然后变量值为undefined
- 寻找函数声明,赋值为该函数的函数体
- 等待执行代码
引申一下,这里可以更深入理解一下函数声明跟函数表达式的区别;
引申两下,实参,形参,跟arguments的关系,还有es6扩展运算符的作用;
前面所说的GO,AO其实都跟作用域相关,《你不知道的JavaScript》里面讲到作用域(scope)是根据名称查找变量的一套规则,当函数发生嵌套的时候,其实也就是发生了作用域的嵌套,产生了作用域链(scope chain)。
怎么个根据名称查找变量呢?这个变量其实就是GO,AO对象里面的属性
- 它是如何查找的呢?
- 先找自己的AO对象,里面有没有这个变量
- 没有的话,找上面那个函数的AO对象,
- 一直沿着这个作用域链找
- 直到找到GO,这个最顶层的作用域下的GO对象
- 再没有的话,那就是要报错了
引申一下,es6块级作用域
- 作用域链如何串在一起?
先写到这里吧,笔试题还涉及了静态方法和实例方法的区别,这里就不讲了