文章目录
for (var i = 0; i<10; i++) {
setTimeout(function () {
console.log(i);
},0)
}
1 输出什么?为什么?
输出10个10
主要考察js的运行机制,因为js是单线程的,一个时间点只能做一件事(从上到下、依次执行),会先处理同步任务,遇到异步任务会先放到任务队列中进行等待,当同步任务执行完毕后再去任务队列中按顺序执行异步任务。(点击事件onclick、定时器setTimeout和setInterval、ajax都属于异步任务)
上面代码中for循环就是同步代码,setTimeout是异步任务,所以等for循环执行完了(此时i已经是10),才会去执行setTimeout。这里又因为i是全局变量,最后一次i++使得i=10,setTimeout是在循环外面执行。for循环结束后,执行setTimeout 输出为10,任务队列中的剩余 9 个 setTimeout 也依次执行,输出为 10。
2 怎么实现输出0,1,2,3,4,。。。,9?
- 将var改为let
- 写成立即函数形式
当for循环执行时,就会立即执行setTimeout,从而使得到的每个副本i值都不一样,这样就可以得到想要的for循环的结果。for (var i = 0; i<10; i++) { (function f(x){ setTimeout(function () { console.log(x); },0) })(i) }
- 闭包形式(注意下面的黄色高亮括号是一定要有的,不然不会有正确结果)
嗯,,,,,似乎只比上面的立即执行函数形式多了一个return,,,是的!!!(因为上面的setTimeout不是函数,所以使用立即执行函数时要用一个函数包着它)
相似题目
1、将var改为let
2、使用立即执行函数,注意data并没有被赋值
3、使用闭包
3 事件机制讲一下
JS代码运行分为两个阶段:1、预解析:先从头到尾扫一遍,将所有变量定义、函数声明定义都提到上面;2、执行:从上到下执行(setTimeout、setInterval及Ajax中的回调函数、事件中的需要触发执行的函数(如click等))除外。
event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
- 浏览器的Event Loop是在html5的规范中明确定义。
- NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
- libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。
宏队列和微队列
宏队列,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微队列,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
(注:这里只针对浏览器和NodeJS)
执行一个JavaScript代码的具体流程:
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7个步骤;
- 重复第3-7个步骤;
- …
4 闭包
出现原因:受javascript链式作用域链的影响,父级变量中无法访问到子级的变量值
定义:闭包(closure)是指用一个函数去访问另一个函数的内部变量的方式,闭包是一个函数。闭包可以简单理解成定义在一个函数内部的函数,本质上它能将函数内部与函数外部连接起来。
函数的嵌套形成闭包(一个独立的空间)
用处:模仿块级作用域、创建私有变量
优点:
- 闭包可以将变量长期驻留在内存中,它不会释放外部的引用,从而函数内部的值可以得以保留,这样我们在第二次调用的时候,就会从缓存中读取到该对象
- 能模仿块级作用域,避免了全局变量的污染
- 能在对象中创建私有变量
缺点:(常驻内存,会增大内存使用量,使用不当会造成内存泄漏)
- 变量常驻内存会导致占用更多的内存,可能导致内存泄漏,因而建议只在必要的时候考虑使用闭包。解决方法:在退出函数之前将不使用的局部变量null掉;
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
介绍一下你所了解的作用域链,作用域链的尽头是什么?为什么?
- 每一个函数都有一个作用域,比如创建了一个函数,函数里面又包含了一个函数,那么现在又三个作用域,这样就形成了一个作用域链
- 作用域的特点就是,先在自己的变量范围中查找,如果找不到,就会沿着作用域链往上找
作用域链(存在哪里)
5 call,apply,bind的区别与联系
call,apply,bind这三个方法都是继承自Function.prototype中的,属于实例方法。当然,普通的对象、函数、数组都继承了Function.prototype对象中的这三个方法(call,apply,bind),所以这三个方法都可以在对象、数组、函数中使用。
1 console.log(Function.prototype.hasOwnProperty('call')) //true
2 console.log(Function.prototype.hasOwnProperty('apply')) //true
3 console.log(Function.prototype.hasOwnProperty('bind')) //true
(1)Function.prototype.call()
函数实例的call方法,可以指定该函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。并且会立即执行该函数。
如果call方法没有参数,或者参数为null或undefined或者this,则等同于指向全局对象。
如果使用call方法将this关键字指向keith对象,也就是将该函数执行时所在的作用域为keith对象,返回结果为123。
call()方法可以传递两个参数。第一个参数是指定函数内部中this的指向(也就是函数执行时所在的作用域),第二个参数是函数调用时需要传递的参数。
第一个参数是必须的,可以是null、undefined、this,但是不能为空。设置为null、undefined、this表明函数keith此时处于全局作用域(即它原本的作用域)。第二个参数中必须一个个添加。而在apply中必须以数组的形式添加。
call方法的一个应用是调用对象的原生方法。也可以用于将类数组对象转换为数组。
上面代码中,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个方法,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。要注意的是,hasOwnProperty是Object.prototype原生对象的方法,而call是继承自Function.prototype的方法。
(2)Function.prototype.apply()
apply方法的作用与call方法类似,也是改变this指向(函数执行时所在的作用域),然后在指定的作用域中,调用该函数。同时也会立即执行该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。
apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined或者this,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,在调用时传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。
// 1、找出数组中的最大数
let a = [1,2,4,3,5];
console.log(Math.max.apply(null, a)); //5
console.log(Math.max.call(null, 1,2,4,3,5)); //5
// 2、将数组的空元素变为undefined 通过apply方法,利用Array构造函数将数组的空元素变成undefined。
console.log(Array.apply(null, [1, , 3])); // [1, undefined, 3]
//空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined和null。
// 因此,遍历内部元素的时候,会得到不同的结果。
// 3、转换类似数组的对象
1 console.log(Array.prototype.slice.apply({0:1,length:1})); //[1]
2 console.log(Array.prototype.slice.call({0:1,length:1})); //[1]
3 console.log(Array.prototype.slice.apply({0:1,length:2})); //[1,undefined]
4 console.log(Array.prototype.slice.call({0:1,length:2})); //[1,undefined]
1 function keith(a,b,c){
2 return arguments;
3 }
4
5 console.log(Array.prototype.slice.call(keith(2,3,4))); //[2,3,4]
上面第3条中转换类似数组的对象代码的call,apply方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length属性,以及相对应的数字键。
(3)Function.prototype.bind()
bind方法用于指定函数内部的this指向(执行时所在的作用域),然后返回一个新函数。bind方法并非立即执行一个函数。
let test = {
a: 1,
count: function() {
console.log(this.a++);
}
};
test.count(); //1
test.count(); //2
test.count(); //3
上面代码中,如果this.a指向test对象内部的a属性,如果这个方法赋值给另外一个变量,调用时就会出错。
let f = test.count;
console.log(f);//一个函数
f(); //NaN
上面代码中,如果把count方法赋值给f变量,那么this对象指向不再是test对象了,而是window对象。而window.a默认为undefined,进行递增运算之后undefined++就等于NaN。
为了解决这个问题,可以使用bind方法,将test对象里的this绑定到keith对象上,或者是直接调用。
1 var f = test.count.bind(keith);
2 f(); //1
3 f(); //2
4 f(); //3
当然,this也可以绑定到其他对象上。
1 var obj = {
2 a: 100
3 };
4 var f = test.count.bind(obj);
5 f(); //100
6 f(); //101
7 f(); //102
同样,我们也可以给bind方法传递参数,第一个参数如果为null或者undefined或者this,会将函数内部的this对象指向全局环境;第二个为调用时需要的参数,并且传递参数的形式与call方法相同。
1 function test(a, b) {
2 return a + b;
3 }
4 console.log(test.apply(null,[1,4])); //5
5 console.log(test.call(null, 1, 4)); //5
6 console.log(test.bind(null, 1, 4)); //test()
7 console.log(test.bind(null, 1, 4)()); //5
上面代码中,可以看出call,apply,bind三者的区别:call和apply方法都是在调用之后立即执行的。而bind调用之后是返回原函数,需要再调用一次才行,有点像闭包的味道
(4)绑定回调函数的对象
如果在回掉函数中使用this对象,那么this对象是会指向DOM对象,也就是button对象。如果要解决回调函数中this指向问题,可以用如下方法。
1 var o = {
2 f: function() {
3 console.log(this === o);
4 }
5 }
6
7 $('#button').on('click', function() {
8 o.f.apply(o);
9 //或者 o.f.call(o);
10 //或者 o.f.bind(o)();
11 });
点击按钮以后,控制台将会显示true。由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数(而bind方法不会立即执行,注意区别),因此不得不把绑定语句写在一个函数体内。
(5)call,apply,bind方法的联系和区别
其实用于指定函数内部的this指向的问题,这三个方法都差不多,只是存在形式上的差别。总结一下call,apply,bind方法:
a:第一个参数都是指定函数内部中this的指向(函数执行时所在的作用域),然后根据指定的作用域,调用该函数。
b:都可以在函数调用时传递参数。call,bind方法需要直接传入,而apply方法需要以数组的形式传入。
c:call,apply方法是在调用之后立即执行函数,而bind方法没有立即执行,需要将函数再执行一遍。有点闭包的味道。
d:改变this对象的指向问题不仅有call,apply,bind方法,也可以使用that变量来固定this的指向。
6 原型问题
彻底理解JavaScript原型链(一)—__proto__的默认指向
下图两道题
7 判断数据类型的5种方法
1. typeof
- 可以判断数据类型,它返回表示数据类型的字符串(返回结果只能包括number,boolean,string,function,object,undefined);
- 可以使用typeof判断变量是否存在(如if(typeof a!=“undefined”){…});
- Typeof 运算符的问题是无论引用的对象是什么类型 它都返回object
console.log(typeof undefined)//'undefined'
console.log(typeof null) // object
console.log(typeof true) //'boolean'
console.log(typeof 123) //'number'
console.log(typeof "abc") //'string'
console.log(typeof function() {}) //'function'
console.log(typeof {}) //'object'
console.log(typeof [])//'object'
console.log(typeof unknownVariable) //'undefined'
typeof {} // object
typeof [1,2] // object
typeof /\s/ //object
2.instanceof
- 原理 因为A instanceof B 可以判断A是不是B的实例,返回一个布尔值,由构造类型判断出数据类型
var arr=[];
var date = new Date();
console.log(arr instanceof Array)//---------------> true
console.log(date instanceof Date) //---------------> true
//instanceof 后一定为对象类型,区分大小写不能错,该方法适合一些条件选择或分支。
console.log(arr instanceof Array ); // true
console.log(date instanceof Date ); // true
console.log(fn instanceof Function ); // true
//注意: instanceof 后面一定要是对象类型,大小写不能写错,该方法试用一些条件选择或分支
3.通过Object下的toString.call()方法来判断
console.log(toString.call(123)) //[object Number]
console.log(toString.call('123')) //[object String]
console.log(toString.call(undefined)) //[object Undefined]
console.log(toString.call(true)) //[object Boolean]
console.log(toString.call({})) //[object Object]
console.log(toString.call([])) //[object Array]
console.log(toString.call(function(){})) //[object Function]
Object.prototype.toString.call();
console.log(toString.call(123)); //[object Number]
console.log(toString.call('123')); //[object String]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call({})); //[object Object]
console.log(toString.call([])); //[object Array]
console.log(toString.call(function(){})); //[object Function]
4.根据对象的contructor判断
console.log(arr.constructor === Array) //----------> true
console.log(date.constructor === Date) //-----------> true
console.log(fn.constructor === Function); //true
5.jq中判断数据类型的方法
jQuery提供了一系列工具方法,用来判断数据类型,以弥补JavaScript原生的typeof运算符的不足。以下方法对参数进行判断,返回一个布尔值。
jQuery.isArray();是否为数组
jQuery.isEmptyObject();是否为空对象 (不含可枚举属性)。
jQuery.isFunction():是否为函数
jQuery.isNumberic():是否为数字
jQuery.isPlainObject():是否为使用“{}”或“new Object”生成对象,而不是浏览器原生提供的对象。
jQuery.isWindow(): 是否为window对象;
jQuery.isXMLDoc(): 判断一个DOM节点是否处于XML文档中。
8 javascript的数据类型转换
(1)number
console.log(Number());//0
console.log(Number(0));//0
console.log(Number('0'));//0
console.log(Number([0]));//0
console.log(Number(''));//0
console.log(Number([]));//0
console.log(Number(null));//0
console.log(Number(false));//0
console.log(Number({}));//NaN
console.log(Number({a:1}));//NaN
console.log(Number('abcc'));//NaN 字符串
console.log(Number(undefined));//NaN
console.log(Number(NaN));//NaN
Number()转换规则:
1.如果是Boolern值,true和false分别被转换为1和0;
2.如果是null,转为0;
3.如果是undefined,转为NaN;
4.如果是数值,不需要转换,传什么就返回什么;
5.如果是字符串,则需要遵循下面规则转换:
5.1 字符串只把含数字(包含前面带正号+和负号-)的情况,将其转换为十进制数值,前面的0会被忽略。例如‘012’会变成12。
5.2 如果字符串中包含有效的浮点格式,会将其转换为相应的浮点数值,前面的0会被忽略。
5.3 如果字符串包含十六进制格式,则将其转换为相同大小的十进制整数。例如:number(“01f”)=16*1+15=31。
5.4 如果是空字符串,将其转换为0,例如Number(’’)=0。
5.5 如果字符串包含除以上格式之外的字符,则将其转换为NaN。例如:console.log(Number('hello'));//NaN
6.如果是对象,则调用对象的valueOf()方法,然后依照前面的规则转换返回的值,如果转换的结果是NaN,则调用对象的toString()方法,然后再一次按照前面的规则转换返回的字符串值。
转换为字符串
- String(x)
- x.toString(x, 10)
- x+’’
转换为数字
- Number(x)
- parseInt(x, 10)
- parseFloat(x)
- x - 0
- +x
转换为boolean
- Boolean(x)
- !!x
(2)Boolean
除了上面的几个(空、’’、0、null、undefined、NaN)为false,其余情况都为true
注意下面:
内存图
- object存储的是地址
- 基本类型存储的是值
- stack和heap
深复制和浅复制
- 对于简单类型的数据来说,赋值就是深拷贝。
- 对于复杂类型的数据(对象)来说,才要区分浅拷贝和深拷贝。赋值是浅拷贝,拷贝heap内存是深拷贝
9 4399游戏的笔试题(this指向问题)
题一
以下javascript代码执行后,浏览器alert出来的结果分别是?
var color = 'green'
var test4399 = {
color:'blue',
getColor:function(){
var color = 'red';
alert(this.color);
}
}
var getColor = test4399.getColor;
getColor();
test4399.getColor();
知识点1:
this指向:在js中,this总是指向调用它的对象。
场景有4类:
1.有对象就指向调用对象。
2.没有对象就指向全局对象。
3.用new构造就指向新对象。
4.通过apply或call或bind来改变this的所指,绑定传入的第一个参数。
知识点2:
js函数调用时加括号与不加括号区别:
不加括号相当于把函数代码赋值给等号左边,加括号是把函数的返回值赋给等号左边。如:var getColor = test4399.getColor
; test4399.getColor
不加括号相当于把test4399
对象中的getColor
函数的函数代码赋值给等号左边的getColor
。
getColor();
就相当于普通函数的调用。此时的this.color
中的this
指向window
,所以此时this.color
的值应该是全局变量color
的值,即green
。
而test4399.getColor();
此时的this指向调用函数的对象test4399
,因此this.color
值应该是对象的属性值blue
。
所以这道题的答案是:green blue
【题目延伸】
延伸1:
如果去掉this
,直接alert(color)
,浏览器aler
t出来的结果两次都是red
。此时是getColor
函数内部color
变量覆盖了全局和对象test4399
中的color
变量。
延伸2:
如果getColor
函数内部定义color
变量时,不适用var
声明。即代码直接是color = 'red';
,此时函数getColor
中的color
就变成了全局变量,会覆盖掉全局定义的var color = 'green'
,此时浏览器alert出来的结果分别是:red blue
。
题二
下面代码程序的输出是什么?
var myObject = {
foo:"bar",
func:function() {
var self = this;
console.log(this.foo);
console.log(self.foo);
(function(){
console.log(this.foo);
console.log(self.foo);
}());
}
};
myObject.func();
A bar bar bar bar
B bar bar bar undefined
C bar bar undefined bar
D undefined bar undefined bar
这是百度公司前端工程师职位的笔试题目,主要考察立即执行匿名函数与this。
func
是由myObject
调用,所以this
指向myObject
。self
是this
的副本,这里this
和self
都指向对象myObject
。因此第一个和第二个都输出bar
。
而立即执行匿名函数的执行环境具有全局性,因此this
对象通常指向window
,是由window
调用,使用apply
和call
的情况除外。所以第三个this.foo
输出undefined
。
第四个self.foo
,因此这个IIFE立即执行函数的作用域处于myObject.func()
的作用域中,IIFE所处的本作用域中找不到self
变量,所以通过作用域链向上查找,从包含它的父函数中找到指向对象myObject
的self
,因此输出bar。
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
所以答案是C。运行如下:
10 一元运算
符加和减
如下代码输出的结果是什么?
console.log(1+"2"+"2");
console.log(1+ +"2"+"2");
console.log("A"-"B"+"2");
console.log("A"-"B"+2);
console.log(1+ -"1"+"2");
console.log(+1 +"1"+"2");
A 122 122 NaN NaN 112 112
B 122 32 NaN NaN2 02 22
C 122 32 NaN2 NaN 02 112
D 122 32 NaN2 NaN2 02 112
这道题是百度的笔试题目,主要考察一元运算符加和减。这里先附上答案(选C),然后再学习相关知识点。如下:
1、一元加法
一元加法本质上对数字没有影响,即如果一元加法运算符后面本身跟着的就是一个数字型数据,那么得到的结果就是他本身,无论这个值是正数或负数,也不论这个值是整数、小数、科学计数表示。例如:
+ 34 // 34
+ 3.4 // 3.4
+ 34e3 // 34000
+ 34e-3 // 0.034
+ -34 // -34
但是对于字符串,它会把字符串转换成数字。例如:
console.log(typeof "3"); //string
console.log(typeof +"3"); //number
当一元加法运算符对字符串进行操作时,它计算字符串的方式与 parseInt() 相似,主要的不同是只有对以 “0x” 开头的字符串(表示十六进制数字),一元运算符才能把它转换成十进制的值。因此,用一元加法转换 “010”,得到的总是 10,而 “0xB” 将被转换成 11。
对于运算结果不能转换为数字的,将会返回NaN。例如:
+ 010 // 八进制转换成十进制 10
+ 0xB // 十六进制转换成十进制 11
+ "abc" // NaN
2、一元减法
一元减法就是对数值求负,与一元加法运算符相似,一元减法运算符也会把字符串转换成近似的数字,此外还会对该值求负。例如:
var num="20";
console.log(-num); //-20
console.log(typeof -num); //number
console.log(-"3"); //-3
console.log(typeof -"3"); //number
在上面的代码中,一元减法运算符将把字符串 -“20” 转换成 -20。另外一元减法运算符对十六进制和十进制的处理方式与一元加法运算符相似,只是它还会对该值求负。
【3点主意事项】
多个数字与数字字符串混合运算时,跟操作数(±)的位置有关。操作数在数字字符串后面时直接进行拼接,在前面时,需要注意,视情况而定。如下:
console.log(2+1+ "3"); //33
console.log("3"+2+1); //321
数字字符串之前存在数字中的正负号(+、-)时,会被转换成数字。
console.log(typeof "3"); //string
console.log(typeof +"3"); //number
console.log(typeof -"3"); //number
对于运算结果不能转成数字的,将返回NaN.
console.log('a'*'sd') //NaN
console.log('A'-'B') // NaN
console.log('A'+'B') // AB
console.log(+'abc') // NaN
11 变量回收
下列代码存在几个变量没有被回收?
var i = 1;
var i = 2;
var add = function() {
var i = 0;
return function(){
i++;
console.log(i);
}
}();
add();
A 0个
B 1个
C 2个
D 3个
这道题主要考察作用域与闭包,明白代码回收规则。
此题答案选D
1.js有两种作用域,即全局和局部。只要作用域的生命周期没有结束,那么里面的变量就没有被回收。变量的生命周期从声明开始,局部变量在函数执行完毕后被销毁,全局变量在页面关闭时被销毁。
因此全局变量i
(第二个var i = 2;
会覆盖var i= 1;
,算是1个变量)和add 不会被回收。
2.闭包,闭包就是能够读取其他函数内部数据(变量/函数)的函数。如果作用域中的变量被闭包访问或者引用,那么这个变量就不会被回收。
因此闭包中i
不会被回收。综上总共有3个变量不会被回收。
总结一句话:全局变量和闭包中变量不会被回收,局部变量会被回收,就是函数一旦运行结束,函数内部的东西就会被销毁。