avatar

前端基础面试题

这里有一道前端基础面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Foo() {
getName = function () { alert (1) }
return this
}
Foo.getName = function () { alert (2) }
Foo.prototype.getName = function () { alert (3) }
var getName = function () { alert (4) }
function getName() { alert (5) }

//请写出以下输出结果:
Foo.getName(); // => 2
getName(); // => 4
Foo().getName(); // => 1
getName(); // => 1
new Foo.getName(); // => 2
new Foo().getName(); // => 3
new new Foo().getName(); // => 3

大家尝试着先不要看右边注释的答案, 看下自己做的答案跟正确答案是否相同, 如果全部答对了, 就代表这些知识点你都掌握了, 可以不用往下看了


第一问

第一问的Foo.getName自然是访问Foo函数上存储的静态属性,答案自然是2,这里就不需要解释太多的,一般来说第一问对于稍微懂JS基础的同学来说应该是没问题的,当然我们可以用下面的代码来回顾一下基础,先加深一下了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {
var name = name; //私有属性
this.name = name; //公有属性
function getName() { //私有方法
return name;
}
}
Person.prototype.getName = function() { //公有方法
return this.name;
}
Person.name = 'Foo'; //静态属性
Person.getName = function() { //静态方法
return this.name;
}
var foo = new Person('Foo'); //实例化

注意下面这几点:

  • 调用公有方法,公有属性,我们必需先实例化对象,也就是用new操作符实化对象,就可构造函数实例化对象的方法和属性,并且公有方法是不能调用私有方法和静态方法的

  • 静态方法和静态属性就是我们无需实例化就可以调用

  • 而对象的私有方法和属性,外部是不可以访问的


第二问

直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫 getName 的函数,此处其实有两个坑,一是变量声明提升,二是函数表达式和函数声明的区别。

我们来看看为什么,可参考(1)关于Javascript的函数声明和函数表达式 (2)关于JavaScript的变量提升

在Javascript中,定义函数有两种类型

函数声明

1
2
3
4
// 函数声明
function foo(type) {
return type === 'foo'
}

函数表达式

1
2
3
4
// 函数表达式
var foo = function(type) {
return type === 'foo'
}

那么,先来看一个 在一个程序里面同时用函数声明和函数表达式定义一个名为 getName 的函数 的题目

三次 getName() 分别打印什么

1
2
3
4
5
6
7
8
9
getName() // bar
var getName = function() {
console.log('foo')
}
getName() // foo
function getName() {
console.log('bar')
}
getName() // foo
  • JavaScript 解释器中存在一种变量声明被提升的机制,也就是说函数声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面

  • 而用函数表达式创建的函数是在运行时进行赋值,且要等到表达式赋值完成后才能调用

也许这样解释了还不是很清楚, 其实可以把上面的问题等同于下面的写法

1
2
3
4
5
6
7
8
9
10
function getName() { // 函数声明的提升, 原来第6行的函数getName声明提升到这里了
console.log('bar')
}
var getName // 变量声明的提升, 原来第2行的变量getName声明提升到这里了
getName()
getName = function() {
console.log('foo')
}
getName()
getName()

为什么会两种写法会等同呢,因为 JavaScript 解释器会对 函数声明变量声明进行提升, 且函数声明的提升优于变量声明的提升

注意: 变量的声明会提升 而 变量的赋值是不会提升的!!!

那么现在再来看原题就可以得出答案4了, 而不是5


第三问

Foo().getName() 先执行了 Foo 函数,然后调用 Foo 函数的返回值对象的 getName 属性函数。

Foo 函数的第一句 getName = function() { alert (1) } 是一句函数赋值语句,注意它没有 var 声明,所以先向当前 Foo 函数作用域内寻找 getName 变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有 getName 变量,找到了,也就是第二问中的 alert(4) 函数,将此变量的值赋值为 function() { alert(1) }

此处实际上是将外层作用域内的 getName 函数修改了。

注意:此处若依然没有找到会一直向上查找到 window 对象,若 window 对象中也没有 getName 属性,就在 window 对象中创建一个 getName 变量。

之后 Foo 函数的返回值是 this,而 JS 的 this 问题已经有非常多的文章介绍,这里不再多说,而此处的直接调用方式,this指向 window 对象。

Foo 函数返回的是 window 对象,相当于执行 window.getName(),而 window 中的 getName 已经被修改为 alert(1),所以最终会输出1

此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题


第四问

直接调用 getName 函数,相当于 window.getName(),因为这个变量已经被 Foo 函数执行时修改了,遂结果与第三问相同,为1,也就是说 Foo 执行后把全局的 getName 函数给重写了一次,所以结果就是 Foo() 执行重写的那个 getName 函数


第五问

new Foo.getName(),此处考察的是JS的运算符优先级问题

下面是JS运算符的优先级表格,从高到低排列。可参考MDN运算符优先级

优先级 运算类型 关联性 运算符
19 圆括号 n/a ( … )
18 成员访问 从左到右 … . …
需计算的成员访问 从左到右 … [ … ]
new (带参数列表) n/a new … ( … )
17 函数调用 从左到右 … ( … )
new (无参数列表) 从右到左 new …
16 后置递增 (运算符在后) n/a … ++
后置递减(运算符在后) n/a … –
15 逻辑非 从右到左 ! …
按位非 从右到左 ~ …
一元加法 从右到左 + …
一元减法 从右到左 - …
前置递增 从右到左 ++ …
前置递减 从右到左 – …
typeof 从右到左 typeof …
void 从右到左 void …
delete 从右到左 delete …
14 乘法 从左到右 … * …
除法 从左到右 … / …
取模 从左到右 … % …
13 加法 从左到右 … + …
减法 从左到右 … - …
12 按位左移 从左到右 … << …
按位右移 从左到右 … >> …
无符号右移 从左到右 … >>> …
11 小于 从左到右 … < …
小于等于 从左到右 … <= …
大于 从左到右 … > …
大于等于 从左到右 … >= …
in 从左到右 … in …
instanceof 从左到右 … instanceof …
10 等号 从左到右 … == …
非等号 从左到右 … != …
全等号 从左到右 … === …
非全等号 从左到右 … !== …
9 按位与 从左到右 … & …
8 按位异或 从左到右 … ^ …
7 按位或 从左到右 … 按位或 …
6 逻辑与 从左到右 … && …
5 逻辑或 从左到右 … 逻辑或 …
4 条件运算符 从右到左 … ? … : …
3 赋值 从右到左 … = …
… += …
… -= …
… *= …
… /= …
… %= …
… <<= …
… >>= …
… >>>= …
… &= …
… ^= …
… 或= …
2 yield 从右到左 yield …
yield* 从右到左 yield* …
1 展开运算符 n/a … …
0 逗号 从左到右 … , …

new Foo.getName() 的优先级是这样的,相当于是:new (Foo.getName)()

所以这里实际上将 getName 函数作为了构造函数来执行,遂弹出2。


第六问

这一题比上一题的唯一区别就是在Foo那里多出了一个括号,这个有括号跟没括号我们在第五问的时候也看出来优先级是有区别的

(new Foo()).getName()

这里还有一个小知识点,Foo作为构造函数有返回值,所以这里需要说明下JS中的构造函数返回值问题。

构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。
而在JS中构造函数可以有返回值也可以没有。

没有返回值则按照其他语言一样返回实例化对象。

1
2
3
4
function Foo(name) {
this.name = name
}
console.log(new Foo('foo'))

若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(String,Number,Boolean,Null,Undefined)则与无返回值相同,实际返回其实例化对象。

1
2
3
4
5
function Foo(name) {
this.name = name
return 520
}
console.log(new Foo('foo')) // 还是返回实例化对象

若返回值是引用类型,则实际返回值为这个引用类型。

1
2
3
4
5
6
7
function Foo(name) {
this.name = name
return {
age: 16
}
}
console.log(new Foo('foo')) // 返回 {age: 16}

原题中,由于返回的是 this,而 this 在构造函数中本来就代表当前实例化对象,最终 Foo 函数返回实例化对象。

之后调用实例化对象的 getName 函数,因为在 Foo 构造函数中没有为实例化对象添加任何属性,当前对象的原型对象 prototype 中寻找 getName 函数。

这里再拓展个题外话,如果构造函数和原型链都有相同的方法,那么默认会拿构造函数的公有方法而不是原型链


第七问

new new Foo().getName() 同样是运算符优先级问题。做到这一题其实我已经觉得答案没那么重要了,关键只是考察面试者是否真的知道面试官在考察我们什么。
最终实际执行为:

new ((new Foo()).getName)()

先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new,所以最终结果为3


后续

我们再来把原题的难读加大一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Foo() {
this.getName = function() {
console.log(3);
return {
getName: getName //这个就是第六问中涉及的构造函数的返回值问题
}
}; //这个就是第六问中涉及到的,JS构造函数公有方法和原型链方法的优先级
getName = function() {
console.log(1);
};
return this
}
Foo.getName = function() {
console.log(2);
};
Foo.prototype.getName = function() {
console.log(6);
};
var getName = function() {
console.log(4);
};

function getName() {
console.log(5);
}
//答案:
Foo.getName(); // => 2
getName(); // => 4
console.log(Foo()) // => window
Foo().getName(); // => 1
getName(); // => 1
new Foo.getName(); // => 2
new Foo().getName(); // => 3
new Foo().getName().getName(); // => 3 1
new new Foo().getName(); // => 3
文章作者: Kwin
文章链接: http://kw1n.cn/2020/03/25/20200325-前端基础面试题/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Kwin 's Blog
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论