共计 13557 个字符,预计需要花费 34 分钟才能阅读完成。
对 keep-alive 的了解
HTTP1.0 中默认是在每次申请 / 应答,客户端和服务器都要新建一个连贯,实现之后立刻断开连接,这就是 短连贯 。当应用 Keep-Alive 模式时,Keep-Alive 性能使客户端到服务器端的连贯继续无效,当呈现对服务器的后继申请时,Keep-Alive 性能防止了建设或者从新建设连贯,这就是 长连贯。其应用办法如下:
- HTTP1.0 版本是默认没有 Keep-alive 的(也就是默认会发送 keep-alive),所以要想连贯失去放弃,必须手动配置发送
Connection: keep-alive
字段。若想断开 keep-alive 连贯,需发送Connection:close
字段; - HTTP1.1 规定了默认放弃长连贯,数据传输实现了放弃 TCP 连接不断开,期待在同域名下持续用这个通道传输数据。如果须要敞开,须要客户端发送
Connection:close
首部字段。
Keep-Alive 的 建设过程:
- 客户端向服务器在发送申请报文同时在首部增加发送 Connection 字段
- 服务器收到申请并解决 Connection 字段
- 服务器回送 Connection:Keep-Alive 字段给客户端
- 客户端接管到 Connection 字段
- Keep-Alive 连贯建设胜利
服务端主动断开过程(也就是没有 keep-alive):
- 客户端向服务器只是发送内容报文(不蕴含 Connection 字段)
- 服务器收到申请并解决
- 服务器返回客户端申请的资源并敞开连贯
- 客户端接管资源,发现没有 Connection 字段,断开连接
客户端申请断开连接过程:
- 客户端向服务器发送 Connection:close 字段
- 服务器收到申请并解决 connection 字段
- 服务器回送响应资源并断开连接
- 客户端接管资源并断开连接
开启 Keep-Alive 的 长处:
- 较少的 CPU 和内存的使⽤(因为同时关上的连贯的缩小了);
- 容许申请和应答的 HTTP 管线化;
- 升高拥塞管制(TCP 连贯缩小了);
- 缩小了后续申请的提早(⽆需再进⾏握⼿);
- 报告谬误⽆需敞开 TCP 连;
开启 Keep-Alive 的 毛病:
- 长时间的 Tcp 连贯容易导致系统资源有效占用,节约系统资源。
用过 TypeScript 吗?它的作用是什么?
为 JS 增加类型反对,以及提供最新版的 ES 语法的反对,是的利于团队合作和排错,开发大型项目
事件委托的应用场景
场景:给页面的所有的 a 标签增加 click 事件,代码如下:
document.addEventListener("click", function(e) {if (e.target.nodeName == "A")
console.log("a");
}, false);
然而这些 a 标签可能蕴含一些像 span、img 等元素,如果点击到了这些 a 标签中的元素,就不会触发 click 事件,因为事件绑定上在 a 标签元素上,而触发这些外部的元素时,e.target 指向的是触发 click 事件的元素(span、img 等其余元素)。
这种状况下就能够应用事件委托来解决,将事件绑定在 a 标签的外部元素上,当点击它的时候,就会逐级向上查找,晓得找到 a 标签为止,代码如下:
document.addEventListener("click", function(e) {
var node = e.target;
while (node.parentNode.nodeName != "BODY") {if (node.nodeName == "A") {console.log("a");
break;
}
node = node.parentNode;
}
}, false);
异步任务调度器
形容:实现一个带并发限度的异步调度器 Scheduler,保障同时运行的工作最多有 limit
个。
实现:
class Scheduler {queue = []; // 用队列保留正在执行的工作
runCount = 0; // 计数正在执行的工作个数
constructor(limit) {this.maxCount = limit; // 容许并发的最大个数}
add(time, data){const promiseCreator = () => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(data);
resolve();}, time);
});
}
this.queue.push(promiseCreator);
// 每次增加的时候都会尝试去执行工作
this.request();}
request() {
// 队列中还有工作才会被执行
if(this.queue.length && this.runCount < this.maxCount) {
this.runCount++;
// 执行先退出队列的函数
this.queue.shift()().then(() => {
this.runCount--;
// 尝试进行下一次工作
this.request();});
}
}
}
// 测试
const scheduler = new Scheduler(2);
const addTask = (time, data) => {scheduler.add(time, data);
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输入后果 2 3 1 4
参考前端进阶面试题具体解答
树形构造转成列表
题目形容:
[
{
id: 1,
text: '节点 1',
parentId: 0,
children: [
{
id:2,
text: '节点 1_1',
parentId:1
}
]
}
]
转成
[
{
id: 1,
text: '节点 1',
parentId: 0 // 这里用 0 示意为顶级节点
},
{
id: 2,
text: '节点 1_1',
parentId: 1 // 通过这个字段来确定子父级
}
...
]
实现代码如下:
function treeToList(data) {let res = [];
const dfs = (tree) => {tree.forEach((item) => {if (item.children) {dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}
具体阐明 Event loop
家喻户晓 JS 是门非阻塞单线程语言,因为在最后 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,咱们在多个线程中解决 DOM 就可能会产生问题(一个线程中新加节点,另一个线程中删除节点),当然能够引入读写锁解决这个问题。
JS 在执行的过程中会产生执行环境,这些执行环境会被程序的退出到执行栈中。如果遇到异步的代码,会被挂起并退出到 Task(有多种 task)队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,所以实质上来说 JS 中的异步还是同步行为。
console.log('script start');
setTimeout(function() {console.log('setTimeout');
}, 0);
console.log('script end');
以上代码尽管 setTimeout
延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,有余会主动减少。所以 setTimeout
还是会在 script end
之后打印。
不同的工作源会被调配到不同的 Task 队列中,工作源能够分为 微工作(microtask)和 宏工作(macrotask)。在 ES6 标准中,microtask 称为 jobs
,macrotask 称为 task
。
console.log('script start');
setTimeout(function() {console.log('setTimeout');
}, 0);
new Promise((resolve) => {console.log('Promise')
resolve()}).then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
以上代码尽管 setTimeout
写在 Promise
之前,然而因为 Promise
属于微工作而 setTimeout
属于宏工作,所以会有以上的打印。
微工作包含 process.nextTick
,promise
,Object.observe
,MutationObserver
宏工作包含 script
,setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
很多人有个误区,认为微工作快于宏工作,其实是谬误的。因为宏工作中包含了 script
,浏览器会先执行一个宏工作,接下来有异步代码的话就先执行微工作。
所以正确的一次 Event loop 程序是这样的
- 执行同步代码,这属于宏工作
- 执行栈为空,查问是否有微工作须要执行
- 执行所有微工作
- 必要的话渲染 UI
- 而后开始下一轮 Event loop,执行宏工作中的异步代码
通过上述的 Event loop 程序可知,如果宏工作中的异步代码有大量的计算并且须要操作 DOM 的话,为了更快的 界面响应,咱们能够把操作 DOM 放入微工作中。
Node 中的 Event loop
Node 中的 Event loop 和浏览器中的不雷同。
Node 的 Event loop 分为 6 个阶段,它们会依照程序重复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timer
timers 阶段会执行 setTimeout
和 setInterval
一个 timer
指定的工夫并不是精确工夫,而是在达到这个工夫后尽快执行回调,可能会因为零碎正在执行别的事务而提早。
上限的工夫有一个范畴:[1, 2147483647]
,如果设定的工夫不在这个范畴,将被设置为 1。
I/O
I/O 阶段会执行除了 close 事件,定时器和 setImmediate
的回调
idle, prepare
idle, prepare 阶段外部实现
poll
poll 阶段很重要,这一阶段中,零碎会做两件事件
- 执行到点的定时器
- 执行 poll 队列中的事件
并且当 poll 中没有定时器的状况下,会发现以下两件事件
- 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者零碎限度
-
如果 poll 队列为空,会有两件事产生
- 如果有
setImmediate
须要执行,poll 阶段会进行并且进入到 check 阶段执行setImmediate
- 如果没有
setImmediate
须要执行,会期待回调被退出到队列中并立刻执行回调
- 如果有
如果有别的定时器须要被执行,会回到 timer 阶段执行回调。
check
check 阶段执行 setImmediate
close callbacks
close callbacks 阶段执行 close 事件
并且在 Node 中,有些状况下的定时器执行程序是随机的
setTimeout(() => {console.log('setTimeout');
}, 0);
setImmediate(() => {console.log('setImmediate');
})
// 这里可能会输入 setTimeout,setImmediate
// 可能也会相同的输入,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout
当然在这种状况下,执行程序是雷同的
var fs = require('fs')
fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout');
}, 0);
setImmediate(() => {console.log('immediate');
});
});
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate,所以会立刻跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输入肯定是 setImmediate,setTimeout
下面介绍的都是 macrotask 的执行状况,microtask 会在以上每个阶段实现后立刻执行。
setTimeout(()=>{console.log('timer1')
Promise.resolve().then(function() {console.log('promise1')
})
}, 0)
setTimeout(()=>{console.log('timer2')
Promise.resolve().then(function() {console.log('promise2')
})
}, 0)
// 以上代码在浏览器和 node 中打印状况是不同的
// 浏览器中打印 timer1, promise1, timer2, promise2
// node 中打印 timer1, timer2, promise1, promise2
Node 中的 process.nextTick
会先于其余 microtask 执行。
setTimeout(() => {console.log("timer1");
Promise.resolve().then(function() {console.log("promise1");
});
}, 0);
process.nextTick(() => {console.log("nextTick");
});
// nextTick, timer1, promise1
常见浏览器所用内核
(1)IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
(2)Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,当初是 Blink 内核;
(3)Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
(4)Safari 浏览器内核:Webkit 内核;
(5)Opera 浏览器内核:最后是本人的 Presto 内核,起初退出谷歌大军,从 Webkit 又到了 Blink 内核;
(6)360 浏览器、猎豹浏览器内核:IE + Chrome 双内核;
(7)搜狗、漫游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
(8)百度浏览器、世界之窗内核:IE 内核;
(9)2345 浏览器内核:如同以前是 IE 内核,当初也是 IE + Chrome 双内核了;
(10)UC 浏览器内核:这个众口不一,UC 说是他们本人研发的 U3 内核,但如同还是基于 Webkit 和 Trident,还有说是基于火狐内核。
instanceof
作用:判断对象的具体类型。能够区别 array
和 object
,null
和 object
等。
语法:A instanceof B
如何判断的?: 如果 B 函数的显式原型对象在 A 对象的原型链上,返回true
,否则返回false
。
留神:如果检测原始值,则始终返回 false
。
实现:
function myinstanceof(left, right) {
// 根本数据类型都返回 false,留神 typeof 函数 返回 "function"
if((typeof left !== "object" && typeof left !== "function") || left === null) return false;
let leftPro = left.__proto__; // 取右边的(隐式)原型 __proto__
// left.__proto__ 等价于 Object.getPrototypeOf(left)
while(true) {
// 判断是否到原型链顶端
if(leftPro === null) return false;
// 判断左边的显式原型 prototype 对象是否在右边的原型链上
if(leftPro === right.prototype) return true;
// 原型链查找
leftPro = leftPro.__proto__;
}
}
LRU 算法
实现代码如下:
// 一个 Map 对象在迭代时会依据对象中元素的插入程序来进行
// 新增加的元素会被插入到 map 的开端,整个栈倒序查看
class LRUCache {constructor(capacity) {this.secretKey = new Map();
this.capacity = capacity;
}
get(key) {if (this.secretKey.has(key)) {let tempValue = this.secretKey.get(key);
this.secretKey.delete(key);
this.secretKey.set(key, tempValue);
return tempValue;
} else return -1;
}
put(key, value) {
// key 存在,仅批改值
if (this.secretKey.has(key)) {this.secretKey.delete(key);
this.secretKey.set(key, value);
}
// key 不存在,cache 未满
else if (this.secretKey.size < this.capacity) {this.secretKey.set(key, value);
}
// 增加新 key,删除旧 key
else {this.secretKey.set(key, value);
// 删除 map 的第一个元素,即为最长未应用的
this.secretKey.delete(this.secretKey.keys().next().value);
}
}
}
// let cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// console.log("cache.get(1)", cache.get(1))// 返回 1
// cache.put(3, 3);// 该操作会使得密钥 2 作废
// console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
// cache.put(4, 4);// 该操作会使得密钥 1 作废
// console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
// console.log("cache.get(3)", cache.get(3))// 返回 3
// console.log("cache.get(4)", cache.get(4))// 返回 4
实现模板字符串解析性能
题目形容:
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄 18,性别 undefined
实现代码如下:
function render(template, data) {let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {return data[key];
});
return computed;
}
代码输入后果
async function async1 () {await async2();
console.log('async1');
return 'async1 success'
}
async function async2 () {return new Promise((resolve, reject) => {console.log('async2')
reject('error')
})
}
async1().then(res => console.log(res))
输入后果如下:
async2
Uncaught (in promise) error
能够看到,如果 async 函数中抛出了谬误,就会终止谬误后果,不会持续向下执行。
如果想要让谬误不足之处前面的代码执行,能够应用 catch 来捕捉:
async function async1 () {await Promise.reject('error!!!').catch(e => console.log(e))
console.log('async1');
return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')
这样的输入后果就是:
script start
error!!!
async1
async1 success
深 / 浅拷贝
首先判断数据类型是否为对象,如果是对象(数组 | 对象),则递归(深 / 浅拷贝),否则间接拷贝。
function isObject(obj) {return typeof obj === "object" && obj !== null;}
这个函数只能判断 obj
是否是对象,无奈判断其具体是数组还是对象。
防抖节流
题目形容: 手写防抖节流
实现代码如下:
// 防抖
function debounce(fn, delay = 300) {
// 默认 300 毫秒
let timer;
return function () {
const args = arguments;
if (timer) {clearTimeout(timer);
}
timer = setTimeout(() => {fn.apply(this, args); // 扭转 this 指向为调用 debounce 所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {console.log(111);
}, 1000)
);
// 节流
// 设置一个标记
function throttle(fn, delay) {
let flag = true;
return () => {if (!flag) return;
flag = false;
timer = setTimeout(() => {fn();
flag = true;
}, delay);
};
}
window.addEventListener(
"scroll",
throttle(() => {console.log(111);
}, 1000)
);
代码输入后果
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输入后果如下:
1
看到这个题目,好多的 then,实际上只须要记住一个准则:.then
或 .catch
的参数冀望是函数,传入非函数则会产生 值透传。
第一个 then 和第二个 then 中传入的都不是函数,一个是数字,一个是对象,因而产生了透传,将resolve(1)
的值间接传到最初一个 then 里,间接打印出 1。
代码输入后果
var a, b
(function () {console.log(a);
console.log(b);
var a = (b = 3);
console.log(a);
console.log(b);
})()
console.log(a);
console.log(b);
输入后果:
undefined
undefined
3
3
undefined
3
这个题目和下面题目考查的知识点相似,b 赋值为 3,b 此时是一个全局变量,而将 3 赋值给 a,a 是一个局部变量,所以最初打印的时候,a 仍旧是 undefined。
浏览器的次要组成部分
- ⽤户界⾯ 包含地址栏、后退 / 后退按钮、书签菜单等。除了浏览器主窗⼝显示的您申请的⻚⾯外,其余显示的各个局部都属于⽤户界⾯。
- 浏览器引擎 在⽤户界⾯和出现引擎之间传送指令。
- 出现引擎 负责显示申请的内容。如果申请的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- ⽹络 ⽤于⽹络调⽤,⽐如 HTTP 申请。其接⼝与平台⽆关,并为所有平台提供底层实现。
- ⽤户界⾯后端 ⽤于绘制根本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。
- JavaScript 解释器。⽤于解析和执⾏ JavaScript 代码。
- 数据存储 这是长久层。浏览器须要在硬盘上保留各种数据,例如 Cookie。新的 HTML 标准 (HTML5) 定义了“⽹络数据库”,这是⼀个残缺(然而轻便)的浏览器内数据库。
值得注意的是,和⼤少数浏览器不同,Chrome 浏览器的每个标签⻚都别离对应⼀个出现引擎实例。每个标签⻚都是⼀个独⽴的过程。
代码输入后果
async function async1() {console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {console.log("async2");
}
async1();
console.log('start')
输入后果如下:
async1 start
async2
start
async1 end
代码的执行过程如下:
- 首先执行函数中的同步代码
async1 start
,之后遇到了await
,它会阻塞async1
前面代码的执行,因而会先去执行async2
中的同步代码async2
,而后跳出async1
; - 跳出
async1
函数后,执行同步代码start
; - 在一轮宏工作全副执行完之后,再来执行
await
前面的内容async1 end
。
这里能够了解为 await 前面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。
数组扁平化
ES5 递归写法 —— isArray()、concat()
function flat11(arr) {var res = [];
for (var i = 0; i < arr.length; i++) {if (Array.isArray(arr[i])) {res = res.concat(flat11(arr[i]));
} else {res.push(arr[i]);
}
}
return res;
}
如果想实现第二个参数(指定“拉平”的层数),能够这样实现,前面的几种能够本人相似实现:
function flat(arr, level = 1) {var res = [];
for(var i = 0; i < arr.length; i++) {if(Array.isArray(arr[i]) || level >= 1) {res = res.concat(flat(arr[i]), level - 1);
}
else {res.push(arr[i]);
}
}
return res;
}
ES6 递归写法 — reduce()、concat()、isArray()
function flat(arr) {
return arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []);
}
ES6 迭代写法 — 扩大运算符(…)、some()、concat()、isArray()
ES6 的扩大运算符(…) 只能扁平化一层
function flat(arr) {return [].concat(...arr);
}
全副扁平化 :遍历原数组,若arr
中含有数组则应用一次扩大运算符,直至没有为止。
function flat(arr) {while(arr.some(item => Array.isArray(item))) {arr = [].concat(...arr);
}
return arr;
}
toString/join & split
调用数组的 toString()/join()
办法(它会主动扁平化解决),将数组变为字符串而后再用 split
宰割还原为数组。因为 split
宰割后造成的数组的每一项值为字符串,所以须要用一个 map
办法遍历数组将其每一项转换为数值型。
function flat(arr){return arr.toString().split(',').map(item => Number(item));
// return arr.join().split(',').map(item => Number(item));
}
应用正则
JSON.stringify(arr).replace(/[|]/g, '')
会先将数组 arr
序列化为字符串,而后应用 replace()
办法将字符串中所有的[
或 ]
替换成空字符,从而达到扁平化解决,此时的后果为 arr
不蕴含 []
的字符串。最初通过JSON.parse()
解析字符串。
function flat(arr) {return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') +"]");
}
类数组转化为数组
类数组是具备 length
属性,但不具备数组原型上的办法。常见的类数组有 arguments
、DOM 操作方法返回的后果 (如document.querySelectorAll('div')
) 等。
扩大运算符(…)
留神:扩大运算符只能作用于 iterable
对象,即领有 Symbol(Symbol.iterator)
属性值。
let arr = [...arrayLike]
Array.from()
let arr = Array.from(arrayLike);
Array.prototype.slice.call()
let arr = Array.prototype.slice.call(arrayLike);
Array.apply()
let arr = Array.apply(null, arrayLike);
concat + apply
let arr = Array.prototype.concat.apply([], arrayLike);
代码输入问题
function A(){}
function B(a){this.a = a;}
function C(a){if(a){this.a = a;}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a);
console.log(new B().a);
console.log(new C(2).a);
输入后果:1 undefined 2
解析:
- console.log(new A().a),new A()为构造函数创立的对象,自身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输入值为 1;
- console.log(new B().a),ew B()为构造函数创立的对象,该构造函数有参数 a,但该对象没有传参,故该输入值为 undefined;
- console.log(new C(2).a),new C()为构造函数创立的对象,该构造函数有参数 a,且传的实参为 2,执行函数外部,发现 if 为真,执行 this.a = 2, 故属性 a 的值为 2。
箭头函数和一般函数有啥区别?箭头函数能当构造函数吗?
- 一般函数通过 function 关键字定义,this 无奈联合词法作用域应用,在运行时绑定,只取决于函数的调用形式,在哪里被调用,调用地位。(取决于调用者,和是否独立运行)
-
箭头函数应用被称为“胖箭头”的操作
=>
定义,箭头函数不利用一般函数 this 绑定的四种规定,而是依据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无奈被批改(new 也不行)。- 箭头函数罕用于回调函数中,包含事件处理器或定时器
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
-
不能通过 new 关键字调用
- 一个函数外部有两个办法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 办法,创立一个实例对象,而后再执行这个函数体,将函数的 this 绑定在这个实例对象上
- 当间接调用时,执行 [[Call]] 办法,间接执行函数体
- 箭头函数没有 [[Construct]] 办法,不能被用作结构函数调用,当应用 new 进行函数调用时会报错。
function foo() {return (a) => {console.log(this.a);
}
}
var obj1 = {a: 2}
var obj2 = {a: 3}
var bar = foo.call(obj1);
bar.call(obj2);