js在面试中会遇到的几个问题(上)
前言:搜集了一些js面试中的问题,以便温故而知新。
变量提升
变量提升,简单的理解,就是把变量提升提到函数的最顶的地方。需要说明的是,变量提升只是提升变量的声明,并不会把赋值也提升上来,没赋值的变量初始值是undefined。可点击查看demo
下面写一个超级变态的例子从上面文章中摘取的,作者没有给解释,我把自己的解释写一下,觉得有出处的请评论。
function Foo() {
getName = function(){ console.log("1"); };
return this;
}
Foo.getName = function() { console.log("2"); };
Foo.prototype.getName = function(){ console.log("3"); };
var getName = function() { console.log("4"); };
function getName(){ console.log("5"); }
Foo.getName(); // 2
getName(); // 4
Foo().getName(); //1 ? 4 ? 2 ?报错 getName(); // ? 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3
作者:_三月
链接:https://www.jianshu.com/p/260610dfb898
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
首先声明提升及变形后的样子如下:
1 function Foo() {
2 getName = function(){ console.log("1"); };
3 return this;//this指代window
4 }
5 var getName;
6 function getName(){ console.log("5"); }
7 Foo.getName = function() { console.log("2"); };
8 Foo.prototype.getName = function(){ console.log("3"); };
9 getName = function() { console.log("4"); };
10 Foo.getName(); // 2
11 getName(); // 4
12 Foo().getName(); //1 ? 4 ? 2 ?报错 getName(); // ? 1
13 new Foo.getName(); // 2
14 new Foo().getName(); // 3
15 new new Foo().getName(); // 3
这里把Foo.getName = function() { console.log(“2”); };这句话理解为Foo()函数为一个对象,Foo.getName ;给Foo增加了一个函数方法。
在这里首先要弄明白几个事、
- Foo在这里是对象,也可能是构造函数
- 若Foo为构造函数的话。2行的那个getName 方法是私有属性,实例对象访问不到。全局更访问不到
- 7行里Foo.getName =function() { console.log(“2”); };的getName 是Foo对象的方法。全局访问不到
-
5行的getName 是全局变量
首先运行10行Foo.getName();Foo是一个对象,Foo.getName是访问对象里的getName属性,此属性为方法getName(),Foo.getName后加一个“()”是立即执行此方法。此方法输出的是console.log(“2”);
然后运行11行getName();调用全局变量,getName在第五行定义,第九行赋值为 function() { console.log(“4”); };
然后运行12行Foo().getName();这句命令分为两步执行第一步Foo()调用执行里面的命令。Foo函数的命令再次为全局变量getName赋值,赋值为function(){ console.log(“1”); }然后return window.第二window.getName();再次调用全局的getName(),此时全局的getName()在第一步的时候赋值为function(){ console.log(“1”); }所以输出1;
然后运行13行;new Foo后面没有加“()”,所以我认为把foo当做普通的对象来用,即调用Foo.getName属性然后再new一下。生成一个实例对象,但是没有保存在变量里。
然后运行14行;new Foo().getName();new Foo()创建实例对象,Foo为构造函数。实例对象调用getName()方法。实例对象只能调用的是原型上的方法。Foo()内的getName()方法没有加this,是私有属性,不能访问,所以只能访问原型链上的。
然后运行15行;new new Foo().getName(),这个和14行就差一个new 和13行那个是一样的效果就是new了一下实例,但是没有保存到变量里,所以结果和new Foo().getName()是一样的。
闭包
百度百科的解释:
闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
MDN:
闭包是函数和声明该函数的词法环境的组合
JavaScript权威指南第六版关于闭包的说明:
JavaScript采用词法作用域,也就是说函数的执行依赖于变量的作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为”闭包”。
当定义一个函数时,它实际上保存了一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加到保存的作用域链上。
- 词法作用域:函数的嵌套关系是定义时决定的,而非调用时决定的,即词法作用域,即嵌套关系是由词法分析时确定的,而非运行时决定。
var v1 = 'global';
var f1 = function(){
console.log(v1);
}
f1(); //global
var f2 = function(){
var v1 = 'local';
f1();
};
f2(); //global
-
全局作用域的变量是全局对象的属性,不论在什么函数中都可以直接访问,而不需要通过全局对象,但加上全局对象,可以提供搜索效率。
满足下列条件的变量属于全局作用域:1.在最外层定义的变量
2.全局对象的属性
3.任何地方隐匿定义的变量。
对于闭包的理解阮一峰老师写的特别通俗易懂,可点击查看,阮一峰老师说这是初学者很有用,所以读懂了也还只是初学者。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是”nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
匿名函数(说到匿名函数了就来看看匿名函数吧)
1)什么是匿名函数?
没有名字的函数,即function后面没有名字。
2)匿名函数的几种呈现方式或调用方法?
第一种:函数表达式
var a=function (){}//这是一个匿名函数,然后复制给了a,此语句就变成了一个函数表达式
a();//调用方式
第二种:立即执行函数
function(){}//这是一个匿名函数,但是这样写会报错。js解释引擎会报语法错误
function(){}()//这种调用方式也会报错,js遇到function 会当做是函数的声明来看,但是找不到函数的name就会报错。
(function(){})()//这种调用方式用“()”括起来就是一个表达式了,就可以调用了。这是自执行函数
(function(){}())//自执行函数的另一种写法。
自执行函数
// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了
// 不过,请注意下一章节的内容解释
var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();
// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在function前面加一元操作符号
!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();
// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率
// http://twitter.com/kuvos/status/18209252090847232
new function () { /* code */ }
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()
3)
闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
有几种方式可以实现继承
参考:https://www.cnblogs.com/humin/p/4556820.html
- 原型链继承
- 构造函数继承
- 实例继承
- 拷贝继承
- 寄生组合继承
首先先要有一个父类:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1.原型链继承
核心: 将父类的实例作为子类的原型
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
特点:
非常纯粹的继承关系,实例是子类的实例,也是父类的实例
父类新增原型方法/原型属性,子类都能访问到
简单,易于实现
缺点:
要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行
无法实现多继承
来自原型对象的引用属性是所有实例共享的(来自父类(构造函数)中的属性是私有属性,不能共享。)
创建子类实例时,无法向父类构造函数传参
构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:
解决了1中,子类实例共享父类引用属性的问题
创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)
缺点:
实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
实例继承
核心:为父类实例添加新特性,作为子类实例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
特点:
不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
实例是父类的实例,不是子类的实例
不支持多继承
拷贝继承
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:
支持多继承
缺点:
效率较低,内存占用高(因为要拷贝父类的属性)
无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特点:
弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用
缺点:
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)