欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

js在面试中会遇到的几个问题(上)

程序员文章站 2023-12-21 22:04:58
...

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

缺点:

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

上一篇:

下一篇: