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]] 函数,对于该函数来说,算法逻辑一般来说如下:

  1. 如果已经是 原始类型,则返回当前值;

  2. 如果需要转 字符串 则先调用 toSting方法,如果此时是 原始类型 则直接返回,否则再调用 valueOf方法并返回结果;

  3. 如果不是 字符串,则先调用 valueOf方法,如果此时是 原始类型 则直接返回,否则再调用 toString方法并返回结果;

  4. 如果都没有 原始类型 返回,则抛出 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方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别,根据自己的实际情况来选择使用。