适合前端入门 / 巩固基础涵盖 10 道高频面试题目录var、let、const 三者的区别是什么什么是变量提升JavaScript 的数据类型有哪些如何判断一个变量的类型 和 有什么区别为什么更推荐使用 什么是闭包闭包有什么应用场景this 的指向有哪几种情况如何改变 this 的指向undefined 和 null 有什么区别什么时候应该用 null什么是事件冒泡和事件捕获如何阻止事件冒泡for...in 和 for...of 有什么区别什么时候该用哪个什么是原型链proto和 prototype 有什么区别什么是防抖和节流分别用在什么场景如何实现1. var、let、const 三者的区别是什么什么是变量提升考点ES6 之前与之后的变量声明方式对比作用域全局作用域、函数作用域、块级作用域的理解JavaScript 引擎的执行机制变量提升答案var、let、const三者有五大核心区别作用域var是函数作用域let/const是块级作用域变量提升var声明会提升并初始化为 undefinedlet/const虽然也提升但在声明前访问会触发暂时性死区TDZ而报错重复声明var允许重复声明let/const不允许赋值要求const必须声明时赋值let/var不要求修改权限const声明的基本类型不可重新赋值但引用类型内部可修改let/var允许修改推荐使用优先级constletvar除非需要兼容非常老的环境。知识点详解三者的核心区别特性varletconst作用域函数作用域块级作用域块级作用域变量提升会提升值为 undefined会提升但存在暂时性死区TDZ会提升但存在暂时性死区TDZ重复声明允许不允许不允许初始值可不赋值可不赋值必须赋值重新赋值允许允许不允许基本类型全局作用域中作为属性会成为 window 属性不会不会代码示例// var: 函数作用域可以重复声明 functiondemo() { varx1; varx2; // 允许不报错 console.log(x); // 2 } // let: 块级作用域不能重复声明 { lety1; lety2; // SyntaxError: Identifier y has already been declared } // const: 块级作用域必须初始化不能重新赋值 constz1; z2; // TypeError: Assignment to constant variable constobj { a: 1 }; obj.a2; // 允许const 保证的是变量指向的地址不变 // obj {}; // 但这会报错什么是变量提升变量提升Hoisting是指 JavaScript 引擎在执行代码之前会将var声明的变量和函数声明提升到当前作用域顶部的行为。console.log(a); // undefined不是报错 vara1; // 实际执行顺序引擎看到的代码 vara; // 提升声明提升到顶部 console.log(a); // undefined a1; // 赋值留在原位let / const 的暂时性死区TDZlet和const虽然也会被提升但它们存在暂时性死区Temporal Dead Zone——在变量声明之前访问会直接报错。console.log(b); // ReferenceError: Cannot access b before initialization letb1; // 对比 var console.log(c); // undefined变量提升了 varc1; // 常见面试陷阱for 循环中的 var for (vari0; i3; i) { setTimeout(() console.log(i), 100); } // 输出: 3, 3, 3因为 var 是函数作用域i 被共享 // 换成 let for (leti0; i3; i) { setTimeout(() console.log(i), 100); } // 输出: 0, 1, 2因为 let 是块级作用域每次循环都有独立的 i函数提升函数声明会整体提升优先级高于变量提升console.log(fn); // [Function: fn] varfn1; functionfn() { return2; } // 实际执行顺序 // 1. 函数声明整体提升 // 2. var fn 提升但已被函数覆盖不改变值 // 3. 输出函数本身 // 4. fn 1 赋值覆盖最佳实践// 优先使用 const —— 数据不可变引用 constPI3.14159; constconfig { apiUrl: https://api.example.com }; // 需要重新赋值时使用 let —— 明确告诉读者这个变量会被修改 letcount0; count; // 计数器的场景 // 避免使用 var —— 函数作用域和变量提升容易导致 bug // 在 ES6 项目中var 几乎没有任何使用场景2. JavaScript 的数据类型有哪些如何判断一个变量的类型考点JavaScript 的类型系统原始类型 vs 引用类型typeof 操作符的局限性instanceof 和 Object.prototype.toString 的使用答案JavaScript 有8 种数据类型7 种原始类型string、number、boolean、undefined、null、symbol、bigint和 1 种引用类型object。类型判断方法typeof适合判断原始类型除 null 外和 function但不能区分数组和对象Object.prototype.toString.call()最可靠返回[object Type]格式可区分所有类型instanceof判断对象的原型链继承关系适合判断自定义类实例Array.isArray()专门判断数组注意typeof null object是 JavaScript 的历史遗留 bug判断 null 要用 null。知识点详解数据类型分类JavaScript 共有8 种数据类型分为两大类原始类型Primitive7 种类型说明示例string字符串hello、worldnumber数字含整数、浮点数、NaN、Infinity42、3.14、NaNboolean布尔值true、falseundefined未定义undefinednull空值nullsymbol唯一标识ES6Symbol(id)bigint大整数ES20209007199254740991n引用类型Reference1 种object对象、数组、函数、日期、正则等typeof 操作符typeof123 // number typeofhello // string typeoftrue // boolean typeofundefined // undefined typeofnull // object ← 历史 bugnull 被误判为 object typeofSymbol(id) // symbol typeof123n // bigint typeof {} // object typeof [] // object ← 数组被误判为 object typeoffunction(){} // function typeofnewDate() // objecttypeof 的局限性typeof有两个主要缺陷// 1. null 被误判为 object typeofnullobject// trueJS 语言设计的历史遗留 bug // 2. 数组和普通对象无法区分 typeof [] object// true typeof {} object// true精确判断类型的方案方案 1Object.prototype.toString这是最可靠的类型判断方法Object.prototype.toString.call(123) // [object Number] Object.prototype.toString.call(hello) // [object String] Object.prototype.toString.call(true) // [object Boolean] Object.prototype.toString.call(undefined) // [object Undefined] Object.prototype.toString.call(null) // [object Null] Object.prototype.toString.call(Symbol(x)) // [object Symbol] Object.prototype.toString.call(123n) // [object BigInt] Object.prototype.toString.call({}) // [object Object] Object.prototype.toString.call([]) // [object Array] Object.prototype.toString.call(function(){}) // [object Function] Object.prototype.toString.call(newDate()) // [object Date] Object.prototype.toString.call(/regex/) // [object RegExp] // 封装成工具函数 functiongetType(val) { returnObject.prototype.toString.call(val).slice(8, -1); } getType([]) // Array getType({}) // Object方案 2instanceofinstanceof用来判断对象的原型链[] instanceofArray // true [] instanceofObject // true数组的原型链中有 Object newDate() instanceofDate// true // 注意instanceof 不能判断原始类型 123instanceofNumber // false123 是原始类型不是对象 newNumber(123) instanceofNumber// true方案 3Array.isArray专门用来判断数组Array.isArray([]) // true Array.isArray({}) // false Array.isArray(string) // false常见面试陷阱// 陷阱1typeof 对 null 的误判 varnnull; if (typeofnobjectn!null) { // 这不是对象但会被 typeof 误判 } // 正确判断 null nnull // true严格相等 // 陷阱2NaN 的判断 varnum0/0; numNaN // falseNaN 不等于任何值包括自身 Number.isNaN(num) // true推荐 isNaN(num) // trueisNaN 会先尝试转换类型有坑 // 陷阱3浮点数比较 0.10.20.3 // false0.1 0.2 0.30000000000000004 Math.abs(0.10.2-0.3) 1e-10 // true正确比较方式3. 和 有什么区别为什么更推荐使用 考点JavaScript 的类型转换机制隐式类型转换的坑严格相等与抽象相等的区别答案抽象相等会在比较前进行隐式类型转换将两边转为相同类型后再比较严格相等直接比较类型和值类型不同直接返回 false。不推荐使用的原因类型转换规则复杂字符串转数字、null undefined、对象转原始值等极易产生 bug代码可读性差读者需要额外理解转换规则如 0、[] 、false 0等陷阱场景推荐使用行为可预测、语义清晰、安全可靠。唯一使用的合理场景是判断null undefined即value null。知识点详解 与 的核心区别特性抽象相等严格相等类型转换会进行隐式类型转换后再比较不会转换类型比较过程两边类型不同时先转换再比较类型不同直接返回 false推荐程度不推荐推荐 的类型转换规则在比较时会触发隐式类型转换转换规则如下// 1. 字符串和数字比较 5 5 // true字符串转数字 5 5 // false类型不同 // 2. 布尔值会转换为数字 true 1 // truetrue 转数字 1 false 0 // truefalse 转数字 0 true 1 // true // 3. null 和 undefined 互等 null undefined // true null undefined // false类型不同 null 0 // falsenull 不等于任何数字 // 4. 对象与原始类型比较 [] // true空数组转空字符串 [] 0 // true空数组转数字 0 {} [object Object] // true // 5. NaN 不等于任何东西 NaN NaN // falseNaN 不等于自身 NaN NaN // false最常见的 陷阱// 陷阱1空字符串 0 // true false // true // 陷阱2数组比较 [1] 1 // true [1] 1 // true [1, 2] 1,2 // true // 陷阱3对象与字符串 { a: 1 } [object Object] // true // 陷阱4连环比较 false 0 // truefalse 0 为 true 0 为 true转换过程图解 的比较流程 两边类型相同 → 直接用严格相等比较实质上 就是 两边类型不同 → 触发隐式转换 null undefined → true特例不转其他类型 原始类型 vs 原始类型 → 转为数字比较 对象 vs 原始类型 → 对象先转为原始类型valueOf/toString 其他情况 → 都为 false为什么更推荐 的行为不可预测依赖隐式转换规则复杂容易写出 bug语义清晰类型和值都必须相等结果可预测代码可维护性阅读 代码时需要额外脑力去理解意图避免边界情况 0、null 0这类陷阱在 下直接返回 false更安全插个小题目 a ! a a是什么评论区留言讨论实际开发建议// 最佳实践始终使用 if (value 5) { /* ... */ } // 判断 null/undefined唯一推荐使用 的场景 if (value null) { /* 相当于 value null || value undefined */ } // 判断 NaN不能用 或 因为 NaN 不等于任何东西 Number.isNaN(value) // 推荐 Object.is(value, NaN) // 也推荐ES6 isNaN(value) // 不推荐会先转数字有坑4. 什么是闭包闭包有什么应用场景考点JavaScript 作用域链Scope Chain的理解执行上下文与变量环境的生命周期函数作为一等公民的特性闭包的实际应用答案闭包是指一个内部函数引用了外部函数的变量即使外部函数已经执行完毕这些被引用的变量也不会被垃圾回收。闭包的应用场景数据私有化/封装将变量隐藏在函数内部只暴露必要的接口函数工厂返回带有预设参数的函数记忆化缓存计算结果提升性能防抖/节流保存定时器状态模块化模拟块级作用域ES6 之前注意使用闭包时注意内存管理避免不必要的长期引用导致内存泄漏。知识点详解什么是闭包当一个内部函数引用了外部函数的变量形成了一个闭包。即使外部函数已经执行完毕这些被引用的变量也不会被垃圾回收因为内部函数仍然持有对它们的引用。function outer() { var a 1; // 外部函数的变量 function inner() { // 内部函数 console.log(a); // 引用了外部函数的变量 a } return inner; // 返回内部函数 } var fn outer(); // outer 执行完毕 fn(); // 输出 1 —— a 仍然存在闭包的工作原理JavaScript 的作用域是词法作用域静态作用域函数在定义时就决定了它的作用域链。闭包正是基于这个机制实现的。作用域链示意j inner 函数被创建时记录下对 outer 作用域的引用 ↓ outer 函数作用域a 变量在这里 ↓ 全局作用域 即使 outer 执行完了只要 inner 还被外部引用 outer 的执行上下文就不会被销毁变量 a 就一直存在。闭包的常见代码模式模式 1函数工厂function makeMultiplier(factor) { return function(num) { return num * factor; }; } var double makeMultiplier(2); var triple makeMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 // factor 变量被闭包保留每个函数有不同的 factor模式 2计数器function createCounter() { var count 0; // 私有变量 return { increment: function() { count; }, decrement: function() { count--; }, getCount: function() { return count; } }; } var counter createCounter(); counter.increment(); counter.increment(); counter.getCount(); // 2 // count 是私有的外部无法直接访问模式 3经典 for 循环问题// 陷阱var 没有块级作用域 for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出: 3, 3, 3var 是函数作用域i 被共享 // 解决用闭包 IIFE 或 let for (var i 0; i 3; i) { (function(j) { setTimeout(() console.log(j), 100); })(i); } // 输出: 0, 1, 2 // 或者更简洁用 let for (let i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出: 0, 1, 2let 有块级作用域每次循环都是新的 i闭包的三大应用场景1. 数据私有化封装function Person(name) { var _name name; // 私有变量外部无法直接访问 this.getName function() { return _name; }; this.setName function(newName) { if (newName) _name newName; }; } var p new Person(张三); console.log(p._name); // undefined无法直接访问 console.log(p.getName()); // 张三 p.setName(李四); console.log(p.getName()); // 李四2. 函数记忆化Memoizationfunction memoize(fn) { var cache {}; // 闭包中的缓存 return function(arg) { if (cache[arg] ! undefined) { return cache[arg]; } var result fn(arg); cache[arg] result; return result; }; } var fib memoize(function(n) { return n 1 ? n : fib(n - 1) fib(n - 2); }); fib(100); // 快速返回缓存了中间结果3. 防抖与节流后续专题详解闭包的副作用内存泄漏闭包会持有对外部变量的引用如果闭包长期存在会导致这些外部变量无法被回收。// 场景事件处理中的闭包 function bindEvents() { var element document.getElementById(btn); var data { /* 大量数据 */ }; element.addEventListener(click, function() { // 这个函数引用了 element 和 data console.log(data); }); // 如果 element 被从 DOM 中移除 // 事件处理函数仍然持有引用data 也无法被回收 element.remove(); } // 解决显式清空不需要的引用 element null;5. this 的指向有哪几种情况如何改变 this 的指向考点JavaScript this 的动态绑定机制函数调用方式对 this 的影响call / apply / bind 的区别和使用答案this的五种指向普通函数调用指向window非严格模式或undefined严格模式对象方法调用指向调用该方法的对象谁调用指向谁构造函数调用指向新创建的实例对象箭头函数没有自己的this它会继承外层作用域的 this无法被改变改变 this 指向的三种方法call(this, arg1, arg2, ...)立即执行参数逐个传入apply(this, [args])立即执行参数以数组传入bind(this, arg1, arg2, ...)返回绑定好 this 的新函数不立即执行知识点详解this 的五种指向情况调用方式this 指向示例普通函数调用非严格模式全局对象window/globalThisfn()普通函数调用严格模式undefineduse strict; fn()对象方法调用调用该方法的对象obj.method()构造函数调用新创建的对象实例new Fn()箭头函数继承外层作用域的 this() {}代码示例// 1. 普通函数调用非严格模式—— this 指向 window function fn() { console.log(this); } fn(); // window浏览器中 // 2. 严格模式下 use strict; function fn2() { console.log(this); } fn2(); // undefined // 3. 对象方法调用 —— this 指向调用方法的对象 var obj { name: 张三, say: function() { console.log(this.name); } }; obj.say(); // 张三 var otherFn obj.say; otherFn(); // undefinedthis 丢失指向 window // 4. 构造函数调用 —— this 指向新实例 function Person(name) { this.name name; } var p new Person(李四); console.log(p.name); // 李四 // 5. 箭头函数 —— 继承外层 this不受调用方式影响 var obj2 { name: 王五, say: () { console.log(this.name); // this 指向外层可能是 window } }; obj2.say(); // undefined箭头函数的 this 是外层作用域的 this即 windowthis 丢失的经典场景// 场景1方法赋值给变量 var person { name: Alice, greet: function() { return this.name; } }; var greet person.greet; greet(); // undefinedthis 丢失 // 场景2作为回调函数传入 setTimeout(person.greet, 100); // undefinedthis 丢失 // 场景3DOM 事件处理 var btn document.getElementById(btn); btn.onclick person.greet; // 点击时 this 指向 btn如何改变 this 的指向有三种方法call、apply、bind。1. call—— 立即调用参数逐个传入function greet(greeting, punctuation) { return greeting , this.name punctuation; } var user { name: 张三 }; greet.call(user, Hello, !); // Hello, 张三! // 第一个参数是 this 指向的对象后面的参数是函数的实参2. apply—— 立即调用参数以数组传入greet.apply(user, [Hello, !]); // Hello, 张三! // 和 call 功能相同只是参数形式不同3. bind—— 返回一个新函数绑定 this 和参数不立即执行var boundGreet greet.bind(user, Hello); boundGreet(?); // Hello, 张三? // bind 返回一个新函数this 被永久绑定不会再改变三者对比方法立即执行参数形式this 可变性call是逐个临时绑定apply是数组临时绑定bind否返回新函数逐个永久绑定箭头函数的 this箭头函数没有自己的this它会继承外层作用域的 this且无法被call/apply/bind改变var obj { name: 箭头, say: () { console.log(this.name); // this 指向外层可能是 window }, sayLater: function() { setTimeout(() { console.log(this.name); // 箭头函数继承 sayLater 的 this }, 100); } }; obj.sayLater(); // 箭头this 是 obj实际应用场景// 应用1数组方法借用 var args [a, b, c]; Array.prototype.push.apply(args, [d, e]); console.log(args); // [a, b, c, d, e] // 应用2类型判断伪数组转数组 function fn() { var args Array.prototype.slice.call(arguments); // 或 Array.from(arguments) // 或 [...arguments] } // 应用3bind 固定回调函数的 this var module { x: 42, getX: function() { return this.x; } }; var unboundGetX module.getX; unboundGetX(); // undefinedthis 丢失 var boundGetX module.getX.bind(module); boundGetX(); // 42this 被固定填空题自测以下每道题请先遮住答案独立完成完成后再对照上方详解补全。1. var、let、const 与变量提升var是__作用域let和const是__作用域。var声明的变量会被提升并初始化为__let/const也会提升但在声明前访问会触发__TDZ抛出__。const声明时__必须/可以不赋初始值且基本类型__能/不能重新赋值。但引用类型内部属性__可以/不可以修改。for循环中使用var时setTimeout中访问的i是__同一个/各自独立的换成let后每次循环有__同一个/各自独立的i。在 ES6 项目中推荐的变量声明优先级是______。参考答案函数作用域块级作用域undefined暂时性死区ReferenceError必须不能可以同一个各自独立的constletvar2. JavaScript 数据类型与类型判断JavaScript 共有__种数据类型其中原始类型有__种__、__、__、__、__、__、__引用类型有__种__。typeof null的结果是__这是 JavaScript 的__历史遗留 bug / 标准行为。typeof []的结果是__typeof function(){}的结果是__。判断数组最可靠的方法是__填 API和__填 API。NaN不等于任何值包括它自己判断 NaN 应该用__或__。参考答案87string、number、boolean、undefined、null、symbol、bigint1objectobject历史遗留 bugobjectfunctionArray.isArray()Object.prototype.toString.call()Number.isNaN()Object.is(value, NaN)3. 与 会进行__隐式/显式类型转换后再比较__会/不会转换类型类型不同直接返回__。null undefined的结果是__null undefined的结果是__。 0的结果是__true 1的结果是__[] 0的结果是__。判断变量是否为null或undefined的推荐写法是__。Object.is()和的区别在于Object.is(NaN, NaN)返回__Object.is(0, -0)返回__。参考答案隐式不会falsetruefalsetruetruetruevalue null或value null || value undefinedtruefalse4. 闭包闭包是指__函数引用了__函数的变量使得即使__函数执行完毕被引用的变量也不会被__回收。闭包常用于实现__私有化/公开化数据和函数工厂。用var写for循环 setTimeout会输出 3 个__填数字因为var是__作用域i被所有循环共享。用let后每次循环有独立的i输出__、__、__。闭包可能造成__内存泄漏/性能提升因为闭包会持有对外部变量的__引用/复制导致这些变量无法被__垃圾回收/内存压缩。参考答案内部外部外部垃圾回收私有化3函数0、1、2内存泄漏引用垃圾回收5. this 指向普通函数调用时非严格模式this指向__严格模式下指向__。对象方法调用时this指向__谁调用指向谁。构造函数调用时this指向__。箭头函数__有/没有自己的this它继承自__外层作用域/全局对象。call和apply的区别是参数形式不同call参数__传入apply参数以__传入。bind与前两者最大的区别是__立即执行/返回新函数。事件回调中this默认指向__元素。参考答案window或globalThisundefined调用该方法的对象新创建的实例对象没有外层作用域逐个数组返回新函数绑定事件的 DOM都答对了吗辛苦了学习讲个笑话奖励一下自己有一天小番茄摔倒了爬起来之后变成---番茄酱。
干货精讲 | 初级JS面试高频考题(一)
适合前端入门 / 巩固基础涵盖 10 道高频面试题目录var、let、const 三者的区别是什么什么是变量提升JavaScript 的数据类型有哪些如何判断一个变量的类型 和 有什么区别为什么更推荐使用 什么是闭包闭包有什么应用场景this 的指向有哪几种情况如何改变 this 的指向undefined 和 null 有什么区别什么时候应该用 null什么是事件冒泡和事件捕获如何阻止事件冒泡for...in 和 for...of 有什么区别什么时候该用哪个什么是原型链proto和 prototype 有什么区别什么是防抖和节流分别用在什么场景如何实现1. var、let、const 三者的区别是什么什么是变量提升考点ES6 之前与之后的变量声明方式对比作用域全局作用域、函数作用域、块级作用域的理解JavaScript 引擎的执行机制变量提升答案var、let、const三者有五大核心区别作用域var是函数作用域let/const是块级作用域变量提升var声明会提升并初始化为 undefinedlet/const虽然也提升但在声明前访问会触发暂时性死区TDZ而报错重复声明var允许重复声明let/const不允许赋值要求const必须声明时赋值let/var不要求修改权限const声明的基本类型不可重新赋值但引用类型内部可修改let/var允许修改推荐使用优先级constletvar除非需要兼容非常老的环境。知识点详解三者的核心区别特性varletconst作用域函数作用域块级作用域块级作用域变量提升会提升值为 undefined会提升但存在暂时性死区TDZ会提升但存在暂时性死区TDZ重复声明允许不允许不允许初始值可不赋值可不赋值必须赋值重新赋值允许允许不允许基本类型全局作用域中作为属性会成为 window 属性不会不会代码示例// var: 函数作用域可以重复声明 functiondemo() { varx1; varx2; // 允许不报错 console.log(x); // 2 } // let: 块级作用域不能重复声明 { lety1; lety2; // SyntaxError: Identifier y has already been declared } // const: 块级作用域必须初始化不能重新赋值 constz1; z2; // TypeError: Assignment to constant variable constobj { a: 1 }; obj.a2; // 允许const 保证的是变量指向的地址不变 // obj {}; // 但这会报错什么是变量提升变量提升Hoisting是指 JavaScript 引擎在执行代码之前会将var声明的变量和函数声明提升到当前作用域顶部的行为。console.log(a); // undefined不是报错 vara1; // 实际执行顺序引擎看到的代码 vara; // 提升声明提升到顶部 console.log(a); // undefined a1; // 赋值留在原位let / const 的暂时性死区TDZlet和const虽然也会被提升但它们存在暂时性死区Temporal Dead Zone——在变量声明之前访问会直接报错。console.log(b); // ReferenceError: Cannot access b before initialization letb1; // 对比 var console.log(c); // undefined变量提升了 varc1; // 常见面试陷阱for 循环中的 var for (vari0; i3; i) { setTimeout(() console.log(i), 100); } // 输出: 3, 3, 3因为 var 是函数作用域i 被共享 // 换成 let for (leti0; i3; i) { setTimeout(() console.log(i), 100); } // 输出: 0, 1, 2因为 let 是块级作用域每次循环都有独立的 i函数提升函数声明会整体提升优先级高于变量提升console.log(fn); // [Function: fn] varfn1; functionfn() { return2; } // 实际执行顺序 // 1. 函数声明整体提升 // 2. var fn 提升但已被函数覆盖不改变值 // 3. 输出函数本身 // 4. fn 1 赋值覆盖最佳实践// 优先使用 const —— 数据不可变引用 constPI3.14159; constconfig { apiUrl: https://api.example.com }; // 需要重新赋值时使用 let —— 明确告诉读者这个变量会被修改 letcount0; count; // 计数器的场景 // 避免使用 var —— 函数作用域和变量提升容易导致 bug // 在 ES6 项目中var 几乎没有任何使用场景2. JavaScript 的数据类型有哪些如何判断一个变量的类型考点JavaScript 的类型系统原始类型 vs 引用类型typeof 操作符的局限性instanceof 和 Object.prototype.toString 的使用答案JavaScript 有8 种数据类型7 种原始类型string、number、boolean、undefined、null、symbol、bigint和 1 种引用类型object。类型判断方法typeof适合判断原始类型除 null 外和 function但不能区分数组和对象Object.prototype.toString.call()最可靠返回[object Type]格式可区分所有类型instanceof判断对象的原型链继承关系适合判断自定义类实例Array.isArray()专门判断数组注意typeof null object是 JavaScript 的历史遗留 bug判断 null 要用 null。知识点详解数据类型分类JavaScript 共有8 种数据类型分为两大类原始类型Primitive7 种类型说明示例string字符串hello、worldnumber数字含整数、浮点数、NaN、Infinity42、3.14、NaNboolean布尔值true、falseundefined未定义undefinednull空值nullsymbol唯一标识ES6Symbol(id)bigint大整数ES20209007199254740991n引用类型Reference1 种object对象、数组、函数、日期、正则等typeof 操作符typeof123 // number typeofhello // string typeoftrue // boolean typeofundefined // undefined typeofnull // object ← 历史 bugnull 被误判为 object typeofSymbol(id) // symbol typeof123n // bigint typeof {} // object typeof [] // object ← 数组被误判为 object typeoffunction(){} // function typeofnewDate() // objecttypeof 的局限性typeof有两个主要缺陷// 1. null 被误判为 object typeofnullobject// trueJS 语言设计的历史遗留 bug // 2. 数组和普通对象无法区分 typeof [] object// true typeof {} object// true精确判断类型的方案方案 1Object.prototype.toString这是最可靠的类型判断方法Object.prototype.toString.call(123) // [object Number] Object.prototype.toString.call(hello) // [object String] Object.prototype.toString.call(true) // [object Boolean] Object.prototype.toString.call(undefined) // [object Undefined] Object.prototype.toString.call(null) // [object Null] Object.prototype.toString.call(Symbol(x)) // [object Symbol] Object.prototype.toString.call(123n) // [object BigInt] Object.prototype.toString.call({}) // [object Object] Object.prototype.toString.call([]) // [object Array] Object.prototype.toString.call(function(){}) // [object Function] Object.prototype.toString.call(newDate()) // [object Date] Object.prototype.toString.call(/regex/) // [object RegExp] // 封装成工具函数 functiongetType(val) { returnObject.prototype.toString.call(val).slice(8, -1); } getType([]) // Array getType({}) // Object方案 2instanceofinstanceof用来判断对象的原型链[] instanceofArray // true [] instanceofObject // true数组的原型链中有 Object newDate() instanceofDate// true // 注意instanceof 不能判断原始类型 123instanceofNumber // false123 是原始类型不是对象 newNumber(123) instanceofNumber// true方案 3Array.isArray专门用来判断数组Array.isArray([]) // true Array.isArray({}) // false Array.isArray(string) // false常见面试陷阱// 陷阱1typeof 对 null 的误判 varnnull; if (typeofnobjectn!null) { // 这不是对象但会被 typeof 误判 } // 正确判断 null nnull // true严格相等 // 陷阱2NaN 的判断 varnum0/0; numNaN // falseNaN 不等于任何值包括自身 Number.isNaN(num) // true推荐 isNaN(num) // trueisNaN 会先尝试转换类型有坑 // 陷阱3浮点数比较 0.10.20.3 // false0.1 0.2 0.30000000000000004 Math.abs(0.10.2-0.3) 1e-10 // true正确比较方式3. 和 有什么区别为什么更推荐使用 考点JavaScript 的类型转换机制隐式类型转换的坑严格相等与抽象相等的区别答案抽象相等会在比较前进行隐式类型转换将两边转为相同类型后再比较严格相等直接比较类型和值类型不同直接返回 false。不推荐使用的原因类型转换规则复杂字符串转数字、null undefined、对象转原始值等极易产生 bug代码可读性差读者需要额外理解转换规则如 0、[] 、false 0等陷阱场景推荐使用行为可预测、语义清晰、安全可靠。唯一使用的合理场景是判断null undefined即value null。知识点详解 与 的核心区别特性抽象相等严格相等类型转换会进行隐式类型转换后再比较不会转换类型比较过程两边类型不同时先转换再比较类型不同直接返回 false推荐程度不推荐推荐 的类型转换规则在比较时会触发隐式类型转换转换规则如下// 1. 字符串和数字比较 5 5 // true字符串转数字 5 5 // false类型不同 // 2. 布尔值会转换为数字 true 1 // truetrue 转数字 1 false 0 // truefalse 转数字 0 true 1 // true // 3. null 和 undefined 互等 null undefined // true null undefined // false类型不同 null 0 // falsenull 不等于任何数字 // 4. 对象与原始类型比较 [] // true空数组转空字符串 [] 0 // true空数组转数字 0 {} [object Object] // true // 5. NaN 不等于任何东西 NaN NaN // falseNaN 不等于自身 NaN NaN // false最常见的 陷阱// 陷阱1空字符串 0 // true false // true // 陷阱2数组比较 [1] 1 // true [1] 1 // true [1, 2] 1,2 // true // 陷阱3对象与字符串 { a: 1 } [object Object] // true // 陷阱4连环比较 false 0 // truefalse 0 为 true 0 为 true转换过程图解 的比较流程 两边类型相同 → 直接用严格相等比较实质上 就是 两边类型不同 → 触发隐式转换 null undefined → true特例不转其他类型 原始类型 vs 原始类型 → 转为数字比较 对象 vs 原始类型 → 对象先转为原始类型valueOf/toString 其他情况 → 都为 false为什么更推荐 的行为不可预测依赖隐式转换规则复杂容易写出 bug语义清晰类型和值都必须相等结果可预测代码可维护性阅读 代码时需要额外脑力去理解意图避免边界情况 0、null 0这类陷阱在 下直接返回 false更安全插个小题目 a ! a a是什么评论区留言讨论实际开发建议// 最佳实践始终使用 if (value 5) { /* ... */ } // 判断 null/undefined唯一推荐使用 的场景 if (value null) { /* 相当于 value null || value undefined */ } // 判断 NaN不能用 或 因为 NaN 不等于任何东西 Number.isNaN(value) // 推荐 Object.is(value, NaN) // 也推荐ES6 isNaN(value) // 不推荐会先转数字有坑4. 什么是闭包闭包有什么应用场景考点JavaScript 作用域链Scope Chain的理解执行上下文与变量环境的生命周期函数作为一等公民的特性闭包的实际应用答案闭包是指一个内部函数引用了外部函数的变量即使外部函数已经执行完毕这些被引用的变量也不会被垃圾回收。闭包的应用场景数据私有化/封装将变量隐藏在函数内部只暴露必要的接口函数工厂返回带有预设参数的函数记忆化缓存计算结果提升性能防抖/节流保存定时器状态模块化模拟块级作用域ES6 之前注意使用闭包时注意内存管理避免不必要的长期引用导致内存泄漏。知识点详解什么是闭包当一个内部函数引用了外部函数的变量形成了一个闭包。即使外部函数已经执行完毕这些被引用的变量也不会被垃圾回收因为内部函数仍然持有对它们的引用。function outer() { var a 1; // 外部函数的变量 function inner() { // 内部函数 console.log(a); // 引用了外部函数的变量 a } return inner; // 返回内部函数 } var fn outer(); // outer 执行完毕 fn(); // 输出 1 —— a 仍然存在闭包的工作原理JavaScript 的作用域是词法作用域静态作用域函数在定义时就决定了它的作用域链。闭包正是基于这个机制实现的。作用域链示意j inner 函数被创建时记录下对 outer 作用域的引用 ↓ outer 函数作用域a 变量在这里 ↓ 全局作用域 即使 outer 执行完了只要 inner 还被外部引用 outer 的执行上下文就不会被销毁变量 a 就一直存在。闭包的常见代码模式模式 1函数工厂function makeMultiplier(factor) { return function(num) { return num * factor; }; } var double makeMultiplier(2); var triple makeMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 // factor 变量被闭包保留每个函数有不同的 factor模式 2计数器function createCounter() { var count 0; // 私有变量 return { increment: function() { count; }, decrement: function() { count--; }, getCount: function() { return count; } }; } var counter createCounter(); counter.increment(); counter.increment(); counter.getCount(); // 2 // count 是私有的外部无法直接访问模式 3经典 for 循环问题// 陷阱var 没有块级作用域 for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出: 3, 3, 3var 是函数作用域i 被共享 // 解决用闭包 IIFE 或 let for (var i 0; i 3; i) { (function(j) { setTimeout(() console.log(j), 100); })(i); } // 输出: 0, 1, 2 // 或者更简洁用 let for (let i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出: 0, 1, 2let 有块级作用域每次循环都是新的 i闭包的三大应用场景1. 数据私有化封装function Person(name) { var _name name; // 私有变量外部无法直接访问 this.getName function() { return _name; }; this.setName function(newName) { if (newName) _name newName; }; } var p new Person(张三); console.log(p._name); // undefined无法直接访问 console.log(p.getName()); // 张三 p.setName(李四); console.log(p.getName()); // 李四2. 函数记忆化Memoizationfunction memoize(fn) { var cache {}; // 闭包中的缓存 return function(arg) { if (cache[arg] ! undefined) { return cache[arg]; } var result fn(arg); cache[arg] result; return result; }; } var fib memoize(function(n) { return n 1 ? n : fib(n - 1) fib(n - 2); }); fib(100); // 快速返回缓存了中间结果3. 防抖与节流后续专题详解闭包的副作用内存泄漏闭包会持有对外部变量的引用如果闭包长期存在会导致这些外部变量无法被回收。// 场景事件处理中的闭包 function bindEvents() { var element document.getElementById(btn); var data { /* 大量数据 */ }; element.addEventListener(click, function() { // 这个函数引用了 element 和 data console.log(data); }); // 如果 element 被从 DOM 中移除 // 事件处理函数仍然持有引用data 也无法被回收 element.remove(); } // 解决显式清空不需要的引用 element null;5. this 的指向有哪几种情况如何改变 this 的指向考点JavaScript this 的动态绑定机制函数调用方式对 this 的影响call / apply / bind 的区别和使用答案this的五种指向普通函数调用指向window非严格模式或undefined严格模式对象方法调用指向调用该方法的对象谁调用指向谁构造函数调用指向新创建的实例对象箭头函数没有自己的this它会继承外层作用域的 this无法被改变改变 this 指向的三种方法call(this, arg1, arg2, ...)立即执行参数逐个传入apply(this, [args])立即执行参数以数组传入bind(this, arg1, arg2, ...)返回绑定好 this 的新函数不立即执行知识点详解this 的五种指向情况调用方式this 指向示例普通函数调用非严格模式全局对象window/globalThisfn()普通函数调用严格模式undefineduse strict; fn()对象方法调用调用该方法的对象obj.method()构造函数调用新创建的对象实例new Fn()箭头函数继承外层作用域的 this() {}代码示例// 1. 普通函数调用非严格模式—— this 指向 window function fn() { console.log(this); } fn(); // window浏览器中 // 2. 严格模式下 use strict; function fn2() { console.log(this); } fn2(); // undefined // 3. 对象方法调用 —— this 指向调用方法的对象 var obj { name: 张三, say: function() { console.log(this.name); } }; obj.say(); // 张三 var otherFn obj.say; otherFn(); // undefinedthis 丢失指向 window // 4. 构造函数调用 —— this 指向新实例 function Person(name) { this.name name; } var p new Person(李四); console.log(p.name); // 李四 // 5. 箭头函数 —— 继承外层 this不受调用方式影响 var obj2 { name: 王五, say: () { console.log(this.name); // this 指向外层可能是 window } }; obj2.say(); // undefined箭头函数的 this 是外层作用域的 this即 windowthis 丢失的经典场景// 场景1方法赋值给变量 var person { name: Alice, greet: function() { return this.name; } }; var greet person.greet; greet(); // undefinedthis 丢失 // 场景2作为回调函数传入 setTimeout(person.greet, 100); // undefinedthis 丢失 // 场景3DOM 事件处理 var btn document.getElementById(btn); btn.onclick person.greet; // 点击时 this 指向 btn如何改变 this 的指向有三种方法call、apply、bind。1. call—— 立即调用参数逐个传入function greet(greeting, punctuation) { return greeting , this.name punctuation; } var user { name: 张三 }; greet.call(user, Hello, !); // Hello, 张三! // 第一个参数是 this 指向的对象后面的参数是函数的实参2. apply—— 立即调用参数以数组传入greet.apply(user, [Hello, !]); // Hello, 张三! // 和 call 功能相同只是参数形式不同3. bind—— 返回一个新函数绑定 this 和参数不立即执行var boundGreet greet.bind(user, Hello); boundGreet(?); // Hello, 张三? // bind 返回一个新函数this 被永久绑定不会再改变三者对比方法立即执行参数形式this 可变性call是逐个临时绑定apply是数组临时绑定bind否返回新函数逐个永久绑定箭头函数的 this箭头函数没有自己的this它会继承外层作用域的 this且无法被call/apply/bind改变var obj { name: 箭头, say: () { console.log(this.name); // this 指向外层可能是 window }, sayLater: function() { setTimeout(() { console.log(this.name); // 箭头函数继承 sayLater 的 this }, 100); } }; obj.sayLater(); // 箭头this 是 obj实际应用场景// 应用1数组方法借用 var args [a, b, c]; Array.prototype.push.apply(args, [d, e]); console.log(args); // [a, b, c, d, e] // 应用2类型判断伪数组转数组 function fn() { var args Array.prototype.slice.call(arguments); // 或 Array.from(arguments) // 或 [...arguments] } // 应用3bind 固定回调函数的 this var module { x: 42, getX: function() { return this.x; } }; var unboundGetX module.getX; unboundGetX(); // undefinedthis 丢失 var boundGetX module.getX.bind(module); boundGetX(); // 42this 被固定填空题自测以下每道题请先遮住答案独立完成完成后再对照上方详解补全。1. var、let、const 与变量提升var是__作用域let和const是__作用域。var声明的变量会被提升并初始化为__let/const也会提升但在声明前访问会触发__TDZ抛出__。const声明时__必须/可以不赋初始值且基本类型__能/不能重新赋值。但引用类型内部属性__可以/不可以修改。for循环中使用var时setTimeout中访问的i是__同一个/各自独立的换成let后每次循环有__同一个/各自独立的i。在 ES6 项目中推荐的变量声明优先级是______。参考答案函数作用域块级作用域undefined暂时性死区ReferenceError必须不能可以同一个各自独立的constletvar2. JavaScript 数据类型与类型判断JavaScript 共有__种数据类型其中原始类型有__种__、__、__、__、__、__、__引用类型有__种__。typeof null的结果是__这是 JavaScript 的__历史遗留 bug / 标准行为。typeof []的结果是__typeof function(){}的结果是__。判断数组最可靠的方法是__填 API和__填 API。NaN不等于任何值包括它自己判断 NaN 应该用__或__。参考答案87string、number、boolean、undefined、null、symbol、bigint1objectobject历史遗留 bugobjectfunctionArray.isArray()Object.prototype.toString.call()Number.isNaN()Object.is(value, NaN)3. 与 会进行__隐式/显式类型转换后再比较__会/不会转换类型类型不同直接返回__。null undefined的结果是__null undefined的结果是__。 0的结果是__true 1的结果是__[] 0的结果是__。判断变量是否为null或undefined的推荐写法是__。Object.is()和的区别在于Object.is(NaN, NaN)返回__Object.is(0, -0)返回__。参考答案隐式不会falsetruefalsetruetruetruevalue null或value null || value undefinedtruefalse4. 闭包闭包是指__函数引用了__函数的变量使得即使__函数执行完毕被引用的变量也不会被__回收。闭包常用于实现__私有化/公开化数据和函数工厂。用var写for循环 setTimeout会输出 3 个__填数字因为var是__作用域i被所有循环共享。用let后每次循环有独立的i输出__、__、__。闭包可能造成__内存泄漏/性能提升因为闭包会持有对外部变量的__引用/复制导致这些变量无法被__垃圾回收/内存压缩。参考答案内部外部外部垃圾回收私有化3函数0、1、2内存泄漏引用垃圾回收5. this 指向普通函数调用时非严格模式this指向__严格模式下指向__。对象方法调用时this指向__谁调用指向谁。构造函数调用时this指向__。箭头函数__有/没有自己的this它继承自__外层作用域/全局对象。call和apply的区别是参数形式不同call参数__传入apply参数以__传入。bind与前两者最大的区别是__立即执行/返回新函数。事件回调中this默认指向__元素。参考答案window或globalThisundefined调用该方法的对象新创建的实例对象没有外层作用域逐个数组返回新函数绑定事件的 DOM都答对了吗辛苦了学习讲个笑话奖励一下自己有一天小番茄摔倒了爬起来之后变成---番茄酱。