JavaScript的数据类型有哪些

JavaScript共有八大数据类型,分别是:UndefinedNullBooleanNumberStringObjectSymbolBigInt

数据类型检测的方法有哪些

  1. typeof

    1
    2
    3
    4
    5
    6
    7
    8
    console.log(typeof 1);               // number
    console.log(typeof true); // boolean
    console.log(typeof 'str'); // string
    console.log(typeof function(){}); // function
    console.log(typeof undefined); // undefined
    console.log(typeof []); // object
    console.log(typeof {}); // object
    console.log(typeof null); // object

    数组、对象、null都会被判断为object,其他判断都正确

  2. instanceof
    instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

    1
    2
    3
    4
    5
    6
    7
    console.log(1 instanceof Number);                    // false
    console.log(true instanceof Boolean); // false
    console.log('str' instanceof String); // false

    console.log([] instanceof Array); // true
    console.log(function(){} instanceof Function); // true
    console.log({} instanceof Object); // true
  3. constructor
    constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

    1
    2
    3
    4
    5
    6
    console.log((1).constructor === Number);    // true
    console.log((true).constructor === Boolean); // true
    console.log(('str').constructor === String); // true
    console.log(([]).constructor === Array); // true
    console.log((function() {}).constructor === Function); // true
    console.log(({}).constructor === Object); // true
  4. Object.prototype.toString.call()

判断数组的方式有哪些

  1. Object.prototype.toString.call()
    1
    Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  2. 原型链
    1
    obj.__proto__ === Array.prototype;
  3. Array.isArray()
  4. instanceof
    1
    obj instanceof Array
  5. Array.prototype.isPrototypeOf

null和undefined区别

  • null:表示空对象,主要用于赋值给一些可能会返回对象的变量,作为初始化。
  • undefined:表示未定义,一般变量声明了但还没有定义的时候会返回 undefined
    1
    2
    3
    null == undefined  // true
    null === undefined // false
    !!null === !!undefined // true

typeof NaN

1
2
3
typeof NaN;  // 'number'
NaN === NaN // false
NaN !== NaN // true

Object.is() 与比较操作符 “===”、“==” 的区别

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
    1
    Object.is(NaN, NaN)   // true

js包装类型

在js中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时,js会在后台隐式的将基本类型的值转换为对象

1
2
3
const a = 'abc'
a.length // 3
a.toUpperCase() // 'ABC'

在访问'abc'.length时,js将'abc'在后台转换成String('abc'),然后再访问其length属性。
js也可以使用Object函数显式地将基本类型转换为包装类型:
1
2
var a = 'abc'
Object(a) // Srting {'abc'}

也可以使用valueOf方法将包装类型倒转成基本类型:
1
2
3
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'

箭头函数与普通函数的区别

  1. 箭头函数比普通函数更加简洁
  2. 箭头函数没有自己的this,箭头函数不会创建自己的this,它只会在自己作用域的上一层继承this。箭头函数中this的指向在它定义时已经确定了,之后不会改变。
  3. 箭头函数继承来的this指向永远不会改变
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var id = 'GLOBAL';
    var obj = {
    id: 'OBJ',
    a: function(){
    console.log(this.id);
    },
    b: () => {
    console.log(this.id);
    }
    };
    obj.a(); // 'OBJ'
    obj.b(); // 'GLOBAL'
    new obj.a() // undefined
    new obj.b() // Uncaught TypeError: obj.b is not a constructor
  4. call()、apply()、bind()等方法不能改变箭头函数中this的指向
    1
    2
    3
    4
    5
    6
    7
    8
    var id = 'Global';
    let fun1 = () => {
    console.log(this.id)
    };
    fun1(); // 'Global'
    fun1.call({id: 'Obj'}); // 'Global'
    fun1.apply({id: 'Obj'}); // 'Global'
    fun1.bind({id: 'Obj'})(); // 'Global'
  5. 箭头函数不能作为构造函数使用
  6. 箭头函数没有自己的arguments
  7. 箭头函数没有prototype
  8. 箭头函数不能用作Generator函数,不能使用yeild关键字

new操作符的实现原理

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象
  3. 让函数的this指向这个对象,并执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

类数组转换为数组的方法

  1. 通过call调用数组的slice方法
    1
    Array.prototype.slice.call(arrayLike);
  2. 通过call调用数组的splice方法
    1
    Array.prototype.splice.call(arrayLike, 0);
  3. 通过apply调用数组的concat方法
    1
    Array.prototype.concat.apply([], arrayLike);
  4. 通过Array.from方法
    1
    Array.from(arrayLike);

数组的原生方法

  • 数组和字符串的转换方法:toString()toLocalString()join(),其中join()方法可以指定转换为字符串时的分隔符。
  • 数组尾部操作的方法 pop()push()push方法可以传入多个参数。
  • 数组首部操作的方法 shift()unshift() 重排序的方法 reverse()sort()sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf()lastIndexOf() 迭代方法 every()some()filter()map()forEach() 方法
  • 数组归并方法 reduce()reduceRight() 方法
    总结:原数组改变的方法有:push pop shift unshift reverse sort splice
    不改变原数组的方法有:concat map filter join every some indexOf slice forEach

for…in和for…of的区别

  • for...of遍历获取的是对象的键值,for...in获取的是对象的键名;
  • for...in会遍历对象的整个原型链,性能非常差不推荐使用,而for...of只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for...in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of只返回数组的下标对应的属性值;
    总结:for...in循环主要是为了遍历对象而生,不适用于遍历数组;for...of循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

数组的遍历方法

方法 是否改变原数组 特点
forEach() 数组方法,不改变原数组,没有返回值
map() 数组方法,不改变原数组,有返回值,可链式调用
filter() 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用
for…of for…of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环
every() 和 some() 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false
find() 和 findIndex() 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值
reduce() 和 reduceRight() 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作

⚠️注意:forEach()能不能改变原数组

  1. 数组的元素是基本类型:无法改变原数组
    1
    2
    3
    4
    5
    let arr = [1, 2, 3];
    arr.forEach((item) => {
    item = item * 2;
    });
    console.log(arr); // [1, 2, 3]
  2. 数组的元素是引用数据类型:直接修改整个元素对象时,无法改变原数组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let arr = [
    { name: '张三', age: 20 },
    { name: '李四', age: 30 },
    ];
    arr.forEach((item) => {
    item = {
    name: '王五',
    age: '18',
    };
    });
    console.log(JSON.stringify(arr)); // [{"name":"张三","age":20},{"name":"李四","age":30}]
  3. 数组的元素是引用数据类型:修改元素对象里的某个属性时,可以改变原数组
    1
    2
    3
    4
    5
    6
    7
    8
    let arr = [
    { name: '张三', age: 20 },
    { name: '李四', age: 30 },
    ];
    arr.forEach((item) => {
    item.name = '王五'
    });
    console.log(JSON.stringify(arr)); // [{"name":"王五","age":20},{"name":"王五","age":30}]

闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
用途:

  • 可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  • 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收

在js中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}

因为setTimeout是个异步函数,所以会先把循环全部执行完毕,这时候i就是6了,所以会输出5次6
解决办法:

  1. 使用闭包
    1
    2
    3
    4
    5
    6
    7
    for (var i = 1; i <= 5; i++) {
    (function(j) {
    setTimeout(function timer() {
    console.log(j)
    }, j * 1000)
    })(i)
    }
  2. 使用setTimeout的第三个参数,这个参数会被当成timer函数的参数传入。
    1
    2
    3
    4
    5
    for (var i = 1; i <= 5; i++) {
    setTimeout( function timer(j) {
    console.log(j)
    }, i * 1000, i )
    }
  3. 使用let定义i(推荐)
    1
    2
    3
    4
    5
    for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
    console.log(i)
    }, i * 1000)
    }

call、apply、bind的区别

  • call()方法的第一个参数也是this的指向,后面传入的是一个参数列表,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次,当第一个参数为null、undefined的时候,默认指向window
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function fn(...args){
    console.log(this, args);
    }
    let obj = {
    myname: '张三'
    }
    fn.call(obj, 1, 2); // this会变成传入的obj,传入的参数必须是一个数组;
    fn(1, 2) // this指向window

    fn.call(null, [1, 2]); // this指向window
    fn.call(undefined, [1, 2]); // this指向window
    实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Function.prototype.mycall = function(thisArg, ...args) {
    // 1.获取需要被执行的函数
    var fn = this

    // 2.对thisArg转成对象类型(防止它传入的是非对象类型)
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

    // 3.调用需要被执行的函数
    thisArg.fn = fn
    var result = thisArg.fn(...args)
    delete thisArg.fn

    // 4.将最终的结果返回出去
    return result
    }
  • apply()方法接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function fn(...args){
    console.log(this, args);
    }
    let obj = {
    myname: '张三'
    }
    fn.apply(obj, [1, 2]); // this会变成传入的obj,传入的参数必须是一个数组;
    fn(1,2) // this指向window

    fn.apply(null, [1, 2]); // this指向window
    fn.apply(undefined, [1, 2]); // this指向window
    实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Function.prototype.myapply = function(thisArg, argArray) {
    // 1.获取到要执行的函数
    var fn = this

    // 2.处理绑定的thisArg
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

    // 3.执行函数
    thisArg.fn = fn
    var result
    argArray = argArray || []
    result = thisArg.fn(...argArray)
    delete thisArg.fn

    // 4.返回结果
    return result
    }
  • bind()方法第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入),改变this指向后不会立即执行,而是返回一个永久改变this指向的函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function fn(...args){
    console.log(this, args);
    }
    let obj = {
    myname: '张三'
    }
    const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
    bindFn(1, 2) // this指向obj
    fn(1, 2) // this指向window
    实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Function.prototype.mybind = function(thisArg, ...argArray) {
    // 1.获取到真实需要调用的函数
    var fn = this

    // 2.绑定this
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

    function proxyFn(...args) {
    // 3.将函数放到thisArg中进行调用
    thisArg.fn = fn
    // 特殊: 对两个传入的参数进行合并
    var finalArgs = [...argArray, ...args]
    var result = thisArg.fn(...finalArgs)
    delete thisArg.fn

    // 4.返回结果
    return result
    }

    return proxyFn
    }

setTimeout、Promise、Async/Await 的区别

  • setTimeout
    1
    2
    3
    4
    5
    6
    console.log('script start')
    setTimeout(function() {
    console.log('settimeout')
    })
    console.log('script end')
    // 输出顺序:script start->script end->settimeout
  • Promise
    Promise本身是同步的立即执行函数,当在executor中执行resolve或者reject的时候,此时是异步操作,会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    console.log('script start')
    let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
    }).then(function () {
    console.log('promise2')
    })
    setTimeout(function() {
    console.log('settimeout')
    })
    console.log('script end')
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
  • async/await
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    async function async1(){
    console.log('async1 start');
    await async2();
    console.log('async1 end')
    }
    async function async2(){
    console.log('async2')
    }
    console.log('script start');
    async1();
    console.log('script end')
    // 输出顺序:script start->async1 start->async2->script end->async1 end

Promise方法

Promise有五个常用的方法:then()、catch()、all()、race()、finally。

  1. then()
    then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。
    then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
    resolve(res);
    })
    }).then(res=>{
    return new Promise((resovle,reject)=>{
    ajax('second').success(function(res){
    resolve(res)
    })
    })
    })
  2. catch()
    该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。
    1
    2
    3
    4
    5
    p.then((data) => {
    console.log('resolved',data);
    }).catch((err) => {
    console.log('rejected',err);
    });
  3. all()
    all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
    resolve(1);
    },2000)
    });
    let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
    resolve(2);
    },1000)
    });
    let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
    resolve(3);
    },3000)
    });
    Promise.all([promise1, promise2, promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3]
    })
  4. race()
    race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
    reject(1);
    },2000)
    });
    let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
    resolve(2);
    },1000)
    });
    let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
    resolve(3);
    },3000)
    });
    Promise.race([promise1,promise2,promise3]).then(res=>{
    console.log(res); //结果:2
    },rej=>{
    console.log(rej)};
    )
    当要做一件事,超过多长时间就不做了,就可以用race()这个方法来解决:
    1
    Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
  5. finally()
    finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
    1
    2
    3
    4
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例

async/await 如何捕获异常

1
2
3
4
5
6
7
async function fn(){
try{
let a = await Promise.reject('error')
}catch(error){
console.log(error)
}
}

JavaScript柯里化

柯里化:是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参
数,而且返回结果的新函数的技术
手动实现一个curry函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 柯里化函数的实现myCurrying
function myCurrying(fn) {
function curried(...args) {
// 判断当前已经接收的参数的个数, 可以参数本身需要接受的参数是否已经一致了
// 1.当已经传入的参数 大于等于 需要的参数时, 就执行函数
if (args.length >= fn.length) {
// fn.call(this, ...args)
return fn.apply(this, args)
} else {
// 没有达到个数时, 需要返回一个新的函数, 继续来接收的参数
function curried2(...args2) {
// 接收到参数后, 需要递归调用curried来检查函数的个数是否达到
return curried.apply(this, args.concat(args2))
}
return curried2
}
}
return curried
}