js基础知识点及面试题(一)
一、数据类型
1.原始类型有哪几种?
答:原始类型有boolean、
null、
undefined、
number、
string、
symbol
原始类型存储的都是值,是没有函数可以调用的、比如 undefined.toString(),当例如'1'.toString()可以调用,是因为此时被强制转换成了
String
类型也就是对象类型,所以可以调用 toString
函数。
2.在js中
答:因为浮点数运算的精度问题,计算机只认识二进制,在进行运算时,需要将其他进制的数值转换成二进制,然后再进行计算。
// 将0.1转换成二进制 console.log(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101 // 将0.2转换成二进制 console.log(0.2.toString(2)); // 0.001100110011001100110011001100110011001100110011001101
所以两者相加后,因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 0.30000000000000004
console.log(0.1+0.2); // 0.30000000000000004
解决办法 parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
3.null是对象吗
答:null不是对象,虽然typeof null
会输出 object,但这是js一个悠久的bug。
null表示准备用来保存对象,还没有真正保存对象的值。从逻辑角度看,null值表示一个空对象指针.
console.log(null==undefined) //true
4.对象类型
原始类型以外的对象类型,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
const a = []
假设常量
a
存放了地址(指针)#001,
在地址
#001
的位置存放了值[]
const b = a b.push(1)
当我们将a赋值给b,复制的是原本变量的地址(指针)#001,也就是说当前变量 b
存放的地址(指针)也是 #001
,当我们进行数据修改的时候,就会修改存放在地址(指针) #001
上的值,也就导致了两个变量的值都发生了改变。
当对象作为函数参数的时候
function test(person) { person.age = 26 person = { name: 'yyy', age: 30 } return person } const p1 = { name: 'yck', age: 25 } const p2 = test(p1) console.log(p1) // -> {name:'yck',age:26} console.log(p2) // -> {name:'yyy',age:30}
函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,当前 p1
的值也被修改了
但是当我们重新为 person分配了一个对象时,
person
拥有了一个新的地址(指针),也就和 p1
没有任何关系了,导致了最终两个变量的值是不相同的。
5.typeof vs instanceof
typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?
答:typeof
对于原始类型来说,除了 null
都可以显示正确的类型,但对于对象来说,除了函数都会显示 object
,所以说 typeof
并不能准确判断变量的类型
typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function'
当想判断一个对象的正确类型,可以考虑使用 instanceof
,因为内部机制是通过原型链来判断的
const Person = function() {} const p1 = new Person() p1 instanceof Person // true var str = 'hello world' str instanceof String // false var str1 = new String('hello world') str1 instanceof String // true
二、类型转换
1.转Boolean
在条件判断时,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为 true
,包括所有对象
2.对象转原始类型
let o = { valueOf() { return 0; } }; console.log(+o); // 0 console.log(1 + o); // 1 console.log(1 - o); // 1 console.log('' + o); // '0' console.log(`${o}`); // '[object Object]'
let a = { valueOf() { return 0 }, toString() { return '1' }, [Symbol.toPrimitive]() { return 2 } } 1 + a // => 3
对象在转换类型的时候,会调用内置的 [[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般来说如下:
-
如果已经是 原始类型,则返回当前值;
-
如果需要转 字符串 则先调用
toSting
方法,如果此时是 原始类型 则直接返回,否则再调用valueOf
方法并返回结果; -
如果不是 字符串,则先调用
valueOf
方法,如果此时是 原始类型 则直接返回,否则再调用toString
方法并返回结果; -
如果都没有 原始类型 返回,则抛出
TypeError
类型错误
例如 如何使a==1 && a==2 && a==3
var a = { arr:[3,2,1], //1 valueOf(){ return this.arr.pop() //this.arr++ } } console.log(a==1 && a==2 && a==3). //true
或者对getter的劫持,getter
就是对象属性在进行查询时会被调用的方法 get
,利用此函数也可以实现题目功能
const e = new Proxy({}, { val: 1, get() { return () => this.val++ } }) console.log( e == 1 && e == 2 && e == 3) //true
3.四则运算符
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11' true + true // 2 4 + [1,2,3] // "41,2,3"
'a' + + 'b' // -> "aNaN"
除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12 4 * [] // 0 4 * [1, 2] // NaN
4.比较运算符
如果是对象,就通过 toPrimitive
转换对象
如果是字符串,就通过 unicode
字符索引来比较
let a = { valueOf() { return 0 }, toString() { return '1' } } a > -1 // true
三、彻底理解this
先看这个小例子
function foo() { console.log(this.a) } var a = 1 foo() const obj = { a: 2, foo: foo } obj.foo() const c = new foo()
//1 2 undefined
- 对于直接调用
foo
来说,不管foo
函数被放在了什么地方,this
一定是window
- 对于
obj.foo()
来说,我们只需要记住,谁调用了函数,谁就是this
,所以在这个场景下foo
函数中的this
就是obj
对象 - 对于
new
的方式来说,this
被永远绑定在了c
上面,不会被任何方式改变this
function a() { var user = "小马"; console.log(this.user); //undefined console.log(this); //Window } a();
this最终指向的是调用它的对象,这里的函数a实际是被Window对象所点出来的
var o = { a:10, b:{ a:12, fn:function(){ console.log(this.a); //12 } } } o.b.fn();
这里同样也是对象o点出来的,但是同样this并没有执行它,因此:
1、如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window,但是我们这里不探讨严格版的问题。
2、如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
3、如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,上面列子可以证明,如果不相信,那么接下来我们继续看几个例子。
//列3
var o = { a:10, b:{ // a:12, fn:function(){ console.log(this.a); //undefined } } } o.b.fn();
尽管对象b中没有属性a,这个this指向的也是对象b,因为this只会指向它的上一级对象,不管这个对象中有没有this要的东西。
还有一种比较特殊的情况,看下面这个例子:
//列4
var o = { a:10, b:{ a:12, fn:function(){ console.log(this.a); //undefined console.log(this); //window } } } var j = o.b.fn; j();
这里this指向的是window
this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的,例子4中虽然函数fn是被对象b所引用,但是在将fn赋值给变量j的时候并没有执行所以最终指向的是window,这和例子3是不一样的,例子3是直接执行了fn。
构造函数版this
function Fn(){ this.user = "小马"; } var a = new Fn(); console.log(a.user); //小马
这里之所以对象a可以点出函数Fn里面的user是因为new关键字可以改变this的指向,将这个this指向对象a,为什么我说a是对象,因为用了new关键字就是创建一个对象实例,理解这句话可以想想我们的例子3,我们这里用变量a创建了一个Fn的实例(相当于复制了一份Fn到对象a里面),此时仅仅只是创建,并没有执行,而调用这个函数Fn的是对象a,那么this指向的自然是对象a,那么为什么对象a中会有user,因为你已经复制了一份Fn函数到对象a中,用了new关键字就等同于复制了一份。
除了上面的这些以外,我们还可以自行改变this的指向,关于自行改变this的指向请看JavaScript中call,apply,bind方法的总结这篇文章,详细的说明了我们如何手动更改this的指向。
当this碰到return
function Fn() { this.user = '小马'; return {}; } var a = new Fn; console.log(a.user); //undefined
function Fn() { this.user = '小马'; return function(){}; } var a = new Fn; console.log(a.user); //undefined
function Fn() { this.user = '小马'; return 1; } var a = new Fn; console.log(a.user); //小马 function Fn() { this.user = '小马'; return undefined; //null } var a = new Fn; console.log(a.user); //小马
什么情况呢?
如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。
还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。
箭头函数的this
箭头函数中的this指向的是定义时的this,而不是执行时的this
或者说箭头函数其实是没有 this
的,箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this。
//定义一个对象 var obj = { x:100, //属性x show(){ //延迟500毫秒,输出x的值 setTimeout( //匿名函数 function(){console.log(this.x);}, 500 ); } }; obj.show();//打印结果:undefined
当代码执行到了setTimeout( )的时候,此时的this已经变成了window对象(setTimeout( )是window对象的方法),已经不再是obj对象了,所以我们用this.x获取的时候,获取的是window.x的值,再加上window上没有定义属性x,所以得到的结果就是:undefined。
/定义一个对象 var obj = { x:100,//属性x show(){ //延迟500毫秒,输出x的值 setTimeout( //不同处:箭头函数 () => { console.log(this.x)}, 500 ); } }; obj.show();//打印结果:100
知识点补充
-
在严格版中的默认的this不再是window,而是undefined。
apply/call/bind
一般用来指定this的环境,在没有学之前,通常会有这些问题。
var a = { user:"小马", fn:function(){ console.log(this.user); } } var b = a.fn; b(); //undefined
我们不得不将这个对象保存到另外的一个变量中,那么就可以通过以下方法。
1.call()
var a = { user:"小马", fn:function(){ console.log(this.user); //小马 } } var b = a.fn; b.call(a);
通过在call方法,给第一个参数添加要把b添加到哪个环境中,简单来说,this就会指向那个对象。
call方法除了第一个参数以外还可以添加多个参数,如下:
var a = { user:"小马", fn:function(e,ee){ console.log(this.user); //小马 console.log(e+ee); //3 } } var b = a.fn; b.call(a,1,2);
2.apply()
apply方法和call方法有些相似,只是第二个参数必须是一个数组
var a = { user:"小马", fn:function(){ console.log(this.user); //小马 } } var b = a.fn; b.apply(a);
var a = { user:"小马", fn:function(e,ee){ console.log(this.user); //小马 console.log(e+ee); //11 } } var b = a.fn; b.apply(a,[10,1]);
//如果call和apply的第一个参数写的是null,那么this指向的是window对象
var a = { user:"小马", fn:function(){ console.log(this); //Window {external: Object, chrome: Object, document: document, a: Object, speechSynthesis: SpeechSynthesis…} } } var b = a.fn; b.apply(null);
3.bind()
var a = { user:"小马", fn:function(){ console.log(this.user); } } var b = a.fn; b.bind(a);
我们发现代码没有被打印,对,这就是bind和call、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。
var a = { user:"小马", fn:function(){ console.log(this.user); //小马 } } var b = a.fn; var c = b.bind(a); c();
bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。
var a = { user:"小马", fn:function(e,d,f){ console.log(this.user); //小马 console.log(e,d,f); //10 1 2 } } var b = a.fn; var c = b.bind(a,10); c(1,2);
手写call、apply、bind
Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
context.fn = this
const args = [...arguments].slice(1)
const result = context.fn(...args)
delete context.fn
return result
}
apply的实现也类似,区别在于对参数的处理
bind
的实现对比其他两个函数略微地复杂了一点,因为 bind
需要返回一个函数,需要判断一些边界问题,以下是 bind
的实现
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
const args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
bind
返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new
的方式,我们先来说直接调用的方式- 对于直接调用来说,这里选择了
apply
的方式实现,但是对于参数需要注意以下情况:因为bind
可以实现类似这样的代码f.bind(obj, 1)(2)
,所以我们需要将两边的参数拼接起来,于是就有了这样的实现args.concat(...arguments)
- 最后来说通过
new
的方式,在之前的章节中我们学习过如何判断this
,对于new
的情况来说,不会被任何方式改变this
,所以对于这种情况我们需要忽略传入的this
总结:call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别,根据自己的实际情况来选择使用。