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的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用

缺点:

调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

猜你喜欢

转载自blog.csdn.net/o_xiaopingguo/article/details/80327361
今日推荐