前端工程师面试题(js)

3,351 阅读17分钟

image.png

正值金九银十的招聘旺季,我把我珍藏整理多年的前端面试题分享给大家,分三部分。这是第二部分,js相关的很有用的基础知识。

1. 如何实现一个LazyMan?

1.1 题目

实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)输出:
Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

以此类推。

这是典型的JavaScript流程控制,问题的关键是如何实现任务的顺序执行。在Express有一个类似的东西叫中间件,这个中间件和我们这里的吃饭、睡觉等任务很类似,每一个中间件执行完成后会调用next()函数,这个函数用来调用下一个中间件。

对于这个问题,我们也可以利用相似的思路来解决,首先创建一个任务队列,然后利用next()函数来控制任务的顺序执行:

1.2 队列实现

function _LazyMan(name){
  this.tasks=[];
  var self=this;
  var fn=(function(n){
    var name=n;
    return function(){
      console.log("Hi! this is "+name+"!");
      self.next();
    }
  })(name);
  this.tasks.push(fn);
  setTimeout(function(){
    self.next();
  },0);  // 在下一个事件循环启动任务
}
/* 事件调度函数 */
_LazyMan.prototype.next=function(){
  var fn=this.tasks.shift();
  fn && fn();
}
_LazyMan.prototype.eat=function(name){
  var self=this;
  var fn=(function(name){
    return function(){
      console.log("Eat "+name+" ~");
      self.next()
    }
  })(name);
  this.tasks.push(fn);
  return this; // 实现链式调用
}
_LazyMan.prototype.sleep=function(time){
  var self=this;
  var fn=(function(time){
    return function(){
      setTimeout(function(){
        console.log("Wake up after "+time+" s!");
        self.next();
      },time*1000);
    }
  })(time);
  this.tasks.push(fn);
  return this;
}
_LazyMan.prototype.sleepFirst=function(time){
  var self=this;
  var fn=(function(time){
    return function(){
      setTimeout(function(){
        console.log("Wake up after "+time+" s!");
      },time*1000);
    }
  })(time);
  this.tasks.unshift(fn);
  return this;
}
/* 封装 */
function LazyMan(name){
  return new _LazyMan(name);
}

1.3 promise实现

lazyman里边含有链式调用,那么每一个子任务 return this;这个程序支持任务优先顺序,那么就需要两个贯穿全场的Promise对象:第一,普通顺序promise;第二,插入顺序promise,同时插入顺序是阻塞普通顺序的,代码如下:

function _LazyMan(name){
  this.orderPromise=this.newPromise(); // 定义顺序promise对象
  this.insertPromise=this.newPromise(); // 定义插入promise对象
  this.order(function(resolve){
    console.log(name);
    resolve();
  })
}

_LazyMan.prototype={
  /*实例化promise对象工厂*/
  newPromise:function(){
    return new Promise(function(resolve,reject){
      resolve();
    })
  },
  order:function(fn){
    var self=this;
    this.orderPromise=this.orderPromise.then(function(){
      return new Promise(function(resolve,reject){
        //如果有insertPromise,阻塞orderPromise.
        self.fir?self.insertPromise.then(function(){
          fn(resolve)
        }):fn(resolve)
      })
    })
  },
  insert:function(fn){
    var self=this;
    this.fir=true;
    this.insertPromise=this.insertPromise.then(function(){
      return new Promise(function(resolve,reject){
        fn(resolve);
        self.fir=false;
      })
    })
  },
  sleepFirst:function(time){
    this.insert(function(resolve){
      setTimeout(function(){
        console.log('wait '+time+' s,other logic');
        resolve();
      },time*1000)
    })
    return this;
  },
  eat:function(something){
    this.order(function(resolve){
      console.log(something+' ~~');
      resolve();
    })
    return this;
  },
  sleep:function(time){
    this.order(function(resolve){
      setTimeout(function(){
        console.log('sleep '+time+' s');
      },time*1000);
    })
    return this;
  }
}

//接口封装。
function LazyMan(name) {
    return new _LazyMan(name);
}
//调用测试
LazyMan(‘RoryWu‘).firstTime(1).sleep(2).firstTime(3).eat(‘dinner‘).eat(‘breakfast‘);
// 弹出:
// wait 1 s, other logic
// wait 3 s, other logic
// RoryWu
// sleep 2 s
// dinner~~
// breakfast~~

2. 用JS代码求出页面上一个元素的最终的background-color,不考虑IE浏览器,不考虑元素float情况。

2.1代码实例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style>
    .button {
    height: 2em;
    border: 0;
    border-radius: .2em;
    background-color: #34538b;
    color: #fff;
    font-size: 12px;
    font-weight: bold;
}
  </style>
</head>
<body>
  <input type="button" id="button" class="button" value="点击我,显示背景色" />
  <script>
    document.getElementById("button").onclick = function() {
    var oStyle =window.getComputedStyle(this, null); // null不是必须
    // 如果考虑IE var oStyle = this.currentStyle? this.currentStyle : window.getComputedStyle(this, null);
    alert(oStyle.getPropertyValue("background-color")); //这里也可以用键值获取,建议用getPropertyValue("background-color")
};
  </script>
</body>
</html>

3. setTimeout(fn,0)

3.1 题目

for (var i = 0; i < 3; i++) {
	setTimeout(function() {
		console.log(i);
	}, 0);
	console.log(i);
}

3.2 题解

结果是:0 1 2 3 3 3 很多公司面试都爱出这道题,此题考察的知识点还是蛮多的。 为了防止初学者栽在此问题上,此文稍微分析一下。 都考察了那些知识点呢? 异步、作用域、闭包,你没听错,是闭包。 我们来简化此题:

setTimeout(function() {
        console.log(1);
}, 0);
console.log(2);

先打印2,后打印1。 因为是setTimeout是异步的。 正确的理解setTimeout的方式(注册事件): 有两个参数,第一个参数是函数,第二参数是时间值。 调用setTimeout时,把函数参数,放到事件队列中。等主程序运行完,再调用。 没啥不好理解的。就像我们给按钮绑定事件一样:

btn.onclick = function() {
        alert(1);
};

这么写完,会弹出1吗。不会!!只是绑定事件而已! 必须等我们去触发事件,比如去点击这个按钮,才会弹出1。 setTimeout也是这样的!只是绑定事件,等主程序运行完毕后,再去调用。 setTimeout的时间值是怎么回事呢? 比如:

setTimeout(fn, 2000)

我们可以理解为2000之后,再放入事件队列中,如果此时队列为空,那么就直接调用fn。如果前面还有其他的事件,那就等待。 因此setTimeout是一个约会从来都不准时的童鞋。 继续看:

setTimeout(function() {
        console.log(i);
}, 0);
var i = 1;

程序会不会报错? 不会!而且还会准确得打印1。 为什么? 因为真正去执行console.log(i)这句代码时,var i = 1已经执行完毕了! 所以我们进行dom操作。可以先绑定事件,然后再去写其他逻辑。

window.onload = function() {
        fn();
}
var fn = function() {
        alert('hello')
};

这么写,完全是可以的。因为异步!

es5中是没有块级作用域的

for (var i = 0; i < 3; i++) {}
console.log(i);

也就说i可以在for循环体外访问到。所以是没有块级作用域。 但此问题在es6里终结了,因为es6,发明了let。 这回我们再来看看原题。 原题使用了for循环。循环的本质是干嘛的? 是为了方便我们程序员,少写重复代码。 让我们倒退50年,原题等价于:

var i = 0;
setTimeout(function() {
	console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
	console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
	console.log(i);
}, 0);
console.log(i);
i++;

因为setTimeout是注册事件。根据前面的讨论,可以都放在后面。 原题又等价于如下的写法:

var i = 0;
console.log(i);
i++;
console.log(i);
i++;
console.log(i);
i++;
setTimeout(function() {
	console.log(i);
}, 0);
setTimeout(function() {
	console.log(i);
}, 0);
setTimeout(function() {
	console.log(i);
}, 0);

这回你明白了为啥结果是0 1 2 3 3 3了吧。

那个,说它是闭包,又是怎么回事? 为了很好的说明白这个事情,我们把它放到一个函数中:

var fn = function() {
        for (var i = 0; i < 3; i++) {
                setTimeout(function() {
                        console.log(i);
                }, 0);
                console.log(i);
        }
};
fn();

上面的函数跟我们常见另一个例子(div绑定事件)有什么区别:

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                divs[i].onclick = function() {
                        alert(i);
                };
        }
};
fn();

点击每个div都会弹出3。道理是一样的。因为alert(i)中的i是fn作用越中的,因而这是闭包。 《javascript忍者秘籍》书里把一个函数能调用全局变量,也称闭包。 因为作者认为全局环境也可以想象成一个大的顶级函数。 怎么保证能弹出0,1, 2呢。 解决之道:以毒攻毒! 再创建个闭包!!

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                divs[i].onclick = (function(i) {
                        return function() {
                                alert(i);
                        };
                })(i);
        }
};
fn();

或者如下的写法:

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                (function(i) {
                        divs[i].onclick = function() {
                                alert(i);
                        };
                })(i);
        }
};
fn();

因此原题如果也想setTimeout也弹出0,1,2的话,改成如下:

for (var i = 0; i < 3; i++) {
	setTimeout((function(i) {
		return function() {
			console.log(i);
		};
	})(i), 0);
	console.log(i);
}

想了解更多关于setTimeout(fn,0),可以参考setTimeout(fn,0)

4. 原型与继承及new过程

4.1 原型和继承

先看题目: 请用js实现一个类P,包含成员变量a,成员变量b,成员函数sum,sum输出a与b的和,a,b默认值都为0。实现一个类M,M继承自P,在P的基础上增加成员变量c,成员函数sum变成输出a,b,c的和。 题目分析 Js所有的函数都有一个prototype属性,这个属性引用了一个对象,即原型对象,也简称原型。这个函数包括构造函数和普通函数,我们讲的更多是构造函数的原型,但是也不能否定普通函数也有原型,实现继承的方法很多,这里使用原型链和构造继承,即组合继承的方式。

function P(a,b){
  this.a=a||0;
  this.b=b||0;
  this.sum=function(){
    return this.a+this.b;
  }
}
function M(a,b,c){
  P.call(this,a,b)
  this.c=c;
  this.sum=function(){
    return this.a+this.b+this.c;
  }
}
M.prototype=new P();
var m=new M(2,2,2);
m.sum()  //输出6

接下来,我们深入js继承,看看js实现继承几种方式的特点。

4.2 js继承的实现方式

既然要实现继承,那么首先我们得有一个父类,代码如下:

// 定义一个动物类
function Animal (name,eye,skin) {
  // 属性
  this.name = name || 'Animal';
  this.eye=eye;
  this.skin=skin;
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

下面给大家列出几种继承方式的实现

4.3 原型链继承

实现父类代码在(4.2 js继承的实现方式中) 核心: 将父类的实例作为子类的原型

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

//&emsp;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

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现

缺点:

  1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的引用属性是所有实例共享的(详细请看附录代码)
  4. 创建子类实例时,无法向父类构造函数传参(即无法像这样var cat=new Cat(hair,eye,skin)传参给父类)

推荐指数:★★(3、4两大致命缺陷)

4.4 构造继承

**核心:**使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

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. 解决了1中,子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数(可通过Animal.call(this,name,eye,skin)或者Animal.apply(this,[name,eye,skin])实现)
  3. 可以实现多继承(call多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

推荐指数:★★(缺点3)

4.5 实例继承

**核心:**为父类实例添加新特性,作为子类实例返回

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

特点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

推荐指数:★★

4.6 拷贝继承

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

特点:

  1. 支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

推荐指数:★(缺点1)

4.7 组合继承

**核心:**通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

// 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

特点:

  1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用

缺点:

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

推荐指数:★★★★(仅仅多消耗了一点内存)

4.8 寄生组合继承

**核心:**通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

// 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

特点:

  1. 堪称完美

缺点:

  1. 实现较为复杂

推荐指数:★★★★(实现复杂,扣掉一颗星)

4.9 附录代码

示例:

function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
  //实例引用属性
  this.features = [];
}
function Cat(name){
}
Cat.prototype = new Animal();

var tom = new Cat('Tom');
var kissy = new Cat('Kissy');

console.log(tom.name); // "Animal"
console.log(kissy.name); // "Animal"
console.log(tom.features); // []
console.log(kissy.features); // []

tom.name = 'Tom-New Name';
tom.features.push('eat');

//针对父类实例值类型成员的更改,不影响
console.log(tom.name); // "Tom-New Name"
console.log(kissy.name); // "Animal"
//针对父类实例引用类型成员的更改,会通过影响其他子类实例
console.log(tom.features); // ['eat']
console.log(kissy.features); // ['eat']

原因分析:

关键点:属性查找过程

执行tom.features.push,首先找tom对象的实例属性(找不到),
那么去原型对象中找,也就是Animal的实例。发现有,那么就直接在这个对象的
features属性中插入值。
在console.log(kissy.features); 的时候。同上,kissy实例上没有,那么去原型上找。
刚好原型上有,就直接返回,但是注意,这个原型对象中features属性值已经变化了。

其实这里主要是理解js值类型和引用类型

4.10 new关键字

假设已经定义了父类Base对象 我们执行如下代码

var obj = new Base();

这样代码的结果是什么,我们在Javascript引擎中看到的对象模型是:

interview01.png
new操作符具体干了什么呢?其实很简单,就干了三件事情。

var obj  = {};
obj.__proto__ = Base.prototype; 
Base.call(obj);  
  • 第一行,我们创建了一个空对象obj
  • 第二行,我们将这个空对象的__proto__成员指向了Base函数对象prototype成员对象
  • 第三行,我们将Base函数对象的this指针替换成obj,然后再调用Base函数 注意:new的过程会执行构造函数Base() 再对空对象进行构造

5. js题(2)

5.1 如何实现懒加载(跟预加载的区别)

5.2 同源,跨域

推荐阅读 浏览器同源政策及其规避方法

5.3 js有几种类型值,画内存图

栈:原始数据类型(Undefined,Null,Boolean,Number、String) 
堆:引用数据类型(对象、数组和函数)

两种类型的区别是:存储位置不同;
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其
在栈中的地址,取得地址后从堆中获得实体

interview09.png

5.4 通用的事件侦听器函数

 // event(事件)工具集,来源:github.com/markyun
    markyun.Event = {
        // 页面加载完成后
        readyEvent : function(fn) {
            if (fn==null) {
                fn=document;
            }
            var oldonload = window.onload;
            if (typeof window.onload != 'function') {
                window.onload = fn;
            } else {
                window.onload = function() {
                    oldonload();
                    fn();
                };
            }
        },
        // 视能力分别使用dom0||dom2||IE方式 来绑定事件
        // 参数: 操作的元素,事件名称 ,事件处理程序
        addEvent : function(element, type, handler) {
            if (element.addEventListener) {
                //事件类型、需要执行的函数、是否捕捉
                element.addEventListener(type, handler, false);
            } else if (element.attachEvent) {
                element.attachEvent('on' + type, function() {
                    handler.call(element);
                });
            } else {
                element['on' + type] = handler;
            }
        },
        // 移除事件
        removeEvent : function(element, type, handler) {
            if (element.removeEventListener) {
                element.removeEventListener(type, handler, false);
            } else if (element.datachEvent) {
                element.detachEvent('on' + type, handler);
            } else {
                element['on' + type] = null;
            }
        },
        // 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
        stopPropagation : function(ev) {
            if (ev.stopPropagation) {
                ev.stopPropagation();
            } else {
                ev.cancelBubble = true;
            }
        },
        // 取消事件的默认行为
        preventDefault : function(event) {
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            }
        },
        // 获取事件目标
        getTarget : function(event) {
            return event.target || event.srcElement;
        },
        // 获取event对象的引用,取到事件的所有信息,确保随时能使用event;
        getEvent : function(e) {
            var ev = e || window.event;
            if (!ev) {
                var c = this.getEvent.caller;
                while (c) {
                    ev = c.arguments[0];
                    if (ev && Event == ev.constructor) {
                        break;
                    }
                    c = c.caller;
                }
            }
            return ev;
        }
    };

5.5 ["1", "2", "3"].map(parseInt) 答案

 [1, NaN, NaN] 因为 parseInt 需要两个参数 (val, radix),
 其中 radix 表示解析时用的基数。
 map 传了 3 个 (element, index, array),对应的 radix 不合法导致解析失败。

5.6 声明提升

5.7 那些操作会造成内存泄漏?

内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。

setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。
闭包、控制台日志、循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)

5.8 Polyfill

polyfill 是“在旧版浏览器上复制标准 API 的 JavaScript 补充”,可以动态地加载 JavaScript 代码或库,在不支持这些标准 API 的浏览器中模拟它们。
例如,geolocation(地理位置)polyfill 可以在 navigator 对象上添加全局的 geolocation 对象,还能添加 getCurrentPosition 函数以及“坐标”回调对象,
所有这些都是 W3C 地理位置 API 定义的对象和函数。因为 polyfill 模拟标准 API,所以能够以一种面向所有浏览器未来的方式针对这些 API 进行开发,
一旦对这些 API 的支持变成绝对大多数,则可以方便地去掉 polyfill,无需做任何额外工作。

做的项目中,有没有用过或自己实现一些 polyfill 方案(兼容性处理方案)?

比如: html5shiv、Geolocation、Placeholder 

5.9 关于arguments

1. 定义 由于JavaScript允许函数有不定数目的参数,所以我们需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

var f = function(one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3

arguments对象除了可以读取参数,还可以为参数赋值(严格模式不允许这种用法)

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1)
// 5

可以通过arguments对象的length属性,判断函数调用时到底带几个参数。

function f() {
  return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0

2. 与数组的关系 需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。

但是,可以通过apply方法,把arguments作为参数传进去,这样就可以让arguments使用数组方法了。

// 用于apply方法
myfunction.apply(obj, arguments).

// 使用与另一个数组合并
Array.prototype.concat.apply([1,2,3], arguments)

要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。

var args = Array.prototype.slice.call(arguments);

// or

var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}

3. callee属性 arguments对象带有一个callee属性,返回它所对应的原函数。

var f = function(one) {
  console.log(arguments.callee === f);
}

f() // true

可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。

4. 题目sum(2)(3)

// 写一个 function 让下面两行代码输出的结果都为 5
console.log(sum(2, 3));
console.log(sum(2)(3));

说实话,第一眼看到的时候心里是有点虚的(因为第一次看到它)。sum(2)(3),这种形式的代码确实少见。但是第一反应就是链式调用。 链式调用我们熟悉啊,特别是 jQuery 里面,我们常常能看到连着写的代码。实现原理就是在方法结束时 return 合适的元素对象。

$('#id').parent().siblings('selector').css({
    color: 'red'
});

这道题考什么呢?认真分析了一下,应该有链式调用,toString,柯里化,数组操作等相关内容。大概这些可以满足需求吧? 如何写代码,脑海中大体上有构思了,但是当时手上仅有笔和纸,思路连不上来啊。还好面前放着一台台式机(嘿嘿嘿,机器上写完再抄回纸上)

我的实现大概是这样的。

var sum = (function() {
    var list = [];

    var add = function() {
        // 拼接数组
        var args = Array.prototype.slice.call(arguments);
        list = list.concat(args);
        return add;
    }
    // 覆盖 toString 方法
    add.toString = function() {
        // 计算总和
        var sum = list.reduce(function(pre, next) {
            return pre + next;
        });
        // 清除记录
        list.length = 0;
        return sum;
    }

    return add;
})();

sum(2, 3);
// 5
sum(2)(3);
// 5

这个方法比较复杂,下面介绍个简便的。

var add = function add() {
    var cache;
    if (arguments.length === 1) {
        cache = arguments[0];
        return function ( number ) {return cache + number;};
    }
    else return arguments[0] + arguments[1];
};

6. js浅复制和深复制

6.1 js浅复制

简单的浅复制实现:

var obj={ a:1,arr:[2,3] };
var shallowObj=shallowCopy(obj);

function shallowCopy(src){
  var dst = {};
  for (var prop in src){
    if(src.hasOwnProperty(prop)){
      dst[prop]=src[prop];
    }
  }

  return dst;
}

因为浅复制只会将对象的各个属性进行依次复制,并不会进行递归复制,而 JavaScript 存储对象都是存地址的,所以浅复制会导致 obj.arr 和 shadowObj.arr 指向同一块内存地址,大概的示意图如下。

interview10
导致的结果就是:

shadowObj.arr[1] = 5;
obj.arr[1]   // = 5

6.2 js深复制

而深复制则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。这就不会存在上面 obj 和 shadowObj 的 arr 属性指向同一个对象的问题。

var obj = { a:1, arr: [1,2] };
var obj2 = deepCopy(obj);

结果如下面的示意图所示:

interview11

需要注意的是,如果对象比较大,层级也比较多,深复制会带来性能上的问题。在遇到需要采用深复制的场景时,可以考虑有没有其他替代的方案。在实际的应用场景中,也是浅复制更为常用。

代码递归实现如下:

function deepCopy(o,c){
  var c = c || {}
  for(var i in o){
    if(typeof o[i] == 'object'){
      if(o[i].constructor===Array){
        c[i]=[]
      }else{
        c[i]={}
      }
      deepCopy(o[i],c[i])
    }else{
      c[i]=o[i]
    }
  }
  return c
}

7. jquery题

7.1 jquery与jquery UI

  • jQuery是一个js库,主要提供的功能是选择器,属性修改和事件绑定等等。
  • jQuery UI则是在jQuery的基础上,利用jQuery的扩展性,设计的插件。 提供了一些常用的界面元素,诸如对话框、拖动行为、改变大小行为等等

7.2 jquery扩展

jquery 中如何将数组转化为json字符串,然后再转化回来?

   $.fn.stringifyArray = function(array) {
        return JSON.stringify(array)
    }

    $.fn.parseArray = function(array) {
        return JSON.parse(array)
    }

    然后调用:
    $("").stringifyArray(array)

7.3 jquery的优化方法

针对 jQuery 的优化方法?

*基于Class的选择性的性能相对于Id选择器开销很大,因为需遍历所有DOM元素。

*频繁操作的DOM,先缓存起来再操作。用Jquery的链式调用更好。
 比如:var str=$("a").attr("href");

*for (var i = size; i < arr.length; i++) {}
 for 循环每一次循环都查找了数组 (arr) 的.length 属性,在开始循环的时候设置一个变量来存储这个数字,可以让循环跑得更快:
 for (var i = size, length = arr.length; i < length; i++) {}

在线电子书阅读前端常见面试题汇总

转载请注明

极客教程-极客教程