Appearance
Javascript
JS 相关知识。
一. js 常见模块类型
- CommonJS
- 主要用于服务器端(如 Node.js)。
- 用
require加载模块,module.exports或exports导出。 - 加载模块是同步的。
- AMD(Asynchronous Module Definition)
- 用于浏览器端,解决异步加载。
- 依赖前置,通过
define函数定义模块,回调函数处理模块逻辑。 - 有
require.js库实现该规范,支持异步加载模块。
- UMD(Universal Module Definition)
- 通用模块定义,兼容 CommonJS 和 AMD。
- 可在不同环境(浏览器、Node.js)使用。
- 通过判断环境,采用不同方式加载和定义模块。
- ES6 模块
- 语言层面的模块系统。
- 用
import导入,export导出。 - 静态分析导入导出关系,支持模块的循环引用。
讲讲前端模块化规范
前端模块化主要有几种规范。
- CommonJS主要用于Node.js,同步加载。
- AMD和CMD是为老浏览器设计的异步方案,现在用的少了。
- 目前的主流是官方的ES Module,支持浏览器和Node.js,支持静态优化。
- 而UMD是一种兼容写法,能让一个模块在多种环境下运行。
CJS和ESM最核心的区别
- 加载时机:CJS是运行时加载(动态),ESM是编译时加载(静态)。
- 值拷贝 vs 引用:CJS导出的是值的拷贝,而ESM导出的是引用。
正因为ESM是静态的,所以打包工具才能做Tree-shaking优化,去掉未使用的代码。
运行时加载和编译时加载
- 编译时加载:在代码开始执行之前,模块系统就会扫描所有import语句,构建出完整的模块依赖关系图。
- 运行时加载:在代码执行到require函数时,才会加载并执行相应的模块。
Tree-shaking
Tree-shaking是一种在构建阶段通过静态分析,移除JS代码中未被使用的导出,以减小最终打包体积的性能优化技术。
如何在项目中有效启用Tree-shaking?
1.使用ESM语法:确保代码和第三方库使用import/export。
2.配置生产模式:在Webpack等工具中,设置mode: 'production'通常会默认开启优化。
3.注意副作用:在package.json中通过 "sideEffects"字段标记哪些文件有副作用(如 CSS、Polyfill),防止被误删。
4.按需引入库:避免引入整个库。例如import { debounce } from 'lodash-es'而不是 import _ from 'lodash'。
二. slice 和 substring 和 substr 的区别
substring
在 JavaScript 中,substring()方法用于提取字符串的一部分。它接受两个参数,start 和 end。start 是必需的,表示提取的起始位置(索引从 0 开始),end 是可选的,表示提取的结束位置(不包括该位置的字符)。例如:
如果 start > end,会自动交换这两个参数的位置;如果其中一个小于 0,则替换成 0。
js
let str = "Hello, World!";
let result = str.substring(7, 12);
// result的值为"World",提取从索引7(包含)到索引12(不包含)的字符slice
slice()方法同样用于提取字符串的一部分,它也接受两个参数,start 和 end,含义和 substring 类似,不过 slice()方法在处理 负数 索引时有所不同。负数索引表示从字符串末尾开始计数,例如-1 表示最后一个字符。
js
let str = "Hello, World!";
let result = str.slice(7, -1);
// result的值为"World",提取从索引7(包含)到倒数第一个字符(不包含)的字符三. includes 和 indexOf
indexOf 和 includes 都可以接受第二个可选参数,用于指定开始查找的索引位置。
效率
对于简单数据类型(如数字、字符串等),在大多数现代浏览器中,includes 方法和 indexOf 方法的性能差异不大。它们在底层实现上都需要遍历数组元素来进行查找。不过,includes 方法在语义上更简洁,当只需要判断元素是否存在时,使用 includes 可能更易读。
复杂数据类型查找
当数组元素是复杂数据类型(如对象)时,indexOf 方法使用严格相等(===)来比较元素。这意味着它会比较对象的引用,而不是对象的内容。
js
let obj1 = { name: "John" };
let arr = [obj1];
console.log(arr.indexOf(obj1)); // 0
console.log(arr.includes(obj1)); // true四. 数组 splice()方法
基本语法
js
array.splice(start, deleteCount, item1, item2, ...)主要功能
删除元素
js
let arr = [1, 2, 3, 4, 5];
arr.splice(2, 2); // 从索引2开始删除2个元素
console.log(arr); // [1, 2, 5]添加元素
js
let arr = [1, 2, 3];
arr.splice(1, 0, "a", "b"); // 在索引1位置插入元素
console.log(arr); // [1, 'a', 'b', 2, 3]替换元素
js
let arr = [1, 2, 3, 4];
arr.splice(1, 2, "x", "y"); // 从索引1开始删除2个元素,并插入新元素
console.log(arr); // [1, 'x', 'y', 4]返回值
splice()返回被删除的元素组成的数组:
js
let arr = [1, 2, 3, 4];
let removed = arr.splice(1, 2);
console.log(removed); // [2, 3]
console.log(arr); // [1, 4]实用技巧
- 删除最后一个元素 :arr.splice(-1, 1)
- 清空数组 :arr.splice(0)
- 留下前 n 个元素:arr.splice(n)
- 批量插入 :arr.splice(1, 0, ...['a', 'b', 'c'])
splice()会直接修改原数组,使用时请注意这一点。
五.创建二维数组
如果用 fill 方法创建二维数组,会导致所有子数组引用相同的内存地址,修改一个子数组会影响所有子数组。
因此可以用 map 方法创建二维数组:
js
const arr = new Array(3).fill(0).map(() => new Array(2).fill(0));或者:
js
const dp = Array.from({ length: 3 }, () => new Array(2).fill(0));六. ES6
ES6 (ECMAScript 2015) 为JavaScript 引入了许多重要的新特性。
1.块级作用域(let & const)
let和 const声明的变量具有块级作用域,即只在其所在的 {}内有效,且不存在变量提升。
js
if (true) {
let a = 10;
const B = 20;
// B = 30; // 报错,const声明的常量不可重新赋值
}
// console.log(a); // 报错,a is not definedconst用于声明常量,声明后必须立即赋值,且不能重新赋值。
2.箭头函数
箭头函数没有自己的 this,其内部的 this指向的是定义时所在上下文的 this。
箭头函数不能用作构造函数,不能使用 new命令,也没有 arguments对象。
3.模板字符串与解构赋值
模板字符串使用反引号(`)定义,可以嵌入变量(${expression})和换行。
解构赋值允许从数组或对象中快速提取值。
4.Promise与Async/Await
Promise是处理异步操作的对象,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
js
const promise = new Promise((resolve, reject) => {
// 异步操作,例如请求数据
if (/* 成功 */) {
resolve(value);
} else {
reject(error);
}
});
promise
.then(value => { /* 处理成功结果 */ })
.catch(error => { /* 处理错误 */ });async/await是基于 Promise的语法糖,让异步代码看起来更像同步代码。
js
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}5.Class类
ES6 的 class让对象的原型写法更清晰。
js
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
class Student extends Person {
constructor(name, grade) {
super(name); // 调用父类的constructor
this.grade = grade;
}
study() {
console.log(`${this.name} is studying.`);
}
}6.新的数据结构Set和Map
Set 成员唯一,常用于数组去重。
Map 键值对集合,键可以是任何类型。
7. 迭代器(Iterator)与生成器(Generator)
迭代器协议
迭代器是按需产生序列值的对象,必须实现next()方法:
js
const arrayIterator = [1, 2, 3][Symbol.iterator]();
console.log(arrayIterator.next()); // { value: 1, done: false }
console.log(arrayIterator.next()); // { value: 2, done: false }[10](@ref)生成器函数
生成器是特殊的函数,可以暂停和恢复执行:
js
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next().value); // 1[9](@ref)当生成器函数执行完毕(即 next()方法返回 {done: true})后,生成器对象的状态就永久终止了,无法从中断处恢复执行或重置内部状态。
如果你需要再次从头开始迭代序列,最直接的方法是创建一个新的生成器对象。
8.Symbol
Symbol 是一种新的原始数据类型,用于创建唯一的标识符,解决命名冲突问题。
js
// 每个Symbol都是唯一的
const sym1 = Symbol('id');
const sym2 = Symbol('id');
console.log(sym1 === sym2); // false[1](@ref)
// 用作对象属性名
const obj = {
[Symbol('private')]: 'secret value',
name: 'public value'
};实际应用场景:
- 避免属性冲突:在第三方库开发中防止属性名覆盖
- 模拟私有属性:Symbol属性不会被常规方法遍历到
- 定义内置行为:通过如Symbol.iterator等改变对象默认行为
通过Symbol.iterator可以自定义对象的迭代行为,例如:
js
class students {
members = ["小明", "小王"];
[Symbol.iterator](){
let index = 0;
return {
next: () => {
return {done: index >= this.members.length, value: this.members[index]}
}
}
}
}也可以使用 Generator 函数 实现自定义迭代器:
js
class students {
members = ["小明", "小王"];
*[Symbol.iterator](){
for (let member of this.members) {
yield member;
}
}
}测试一下:
js
let stu = new students();
for (let member of stu) {
console.log(member);
}输出:
小明
小王常见问题
暂时性死区(TDZ)
定义:在代码块内,使用let或const声明变量前,该变量不可访问(引用会报错),这段时间称为暂时性死区(TDZ)。
为什么会有TDZ?
- 避免变量提升导致的逻辑错误(如未声明就使用)
- 使JavaScript更符合静态语言的变量声明规范
如何绕过TDZ?
- 始终在作用域顶部声明变量(最佳实践)
- 使用var(不推荐,破坏代码可维护性)
七、检测数据类型
- typeof:
typeof操作简单易用,可以快速检测基本数据类型。但它无法区分Object、Array和Null,因为都会返回"object"。
示例:
js
console.log(typeof 'Hello, World!'); // 输出'string'
console.log(typeof 3.14); // 输出'number'
console.log(typeof true); // 输出'boolean'
console.log(typeof undefined); // 输出'undefined'
console.log(typeof null); // 输出'object'
console.log(typeof Symbol()); // 输出'symbol'
console.log(typeof 123n); // 输出'bigint'
console.log(typeof {}); // 输出'object'
console.log(typeof []); // 输出'object'
console.log(typeof function() {}); // 输出'function'- instanceof:
主要用于检测引用数据类型、判断一个实例是否属于某个类。
js
console.log([] instanceof Array); // 输出true
console.log({} instanceof Object); // 输出true
console.log(function() {} instanceof Function); // 输出true- Object.prototype.toString.call():
这是一种更通用的方法,可用于检测所有数据类型,包括内置对象和自定义对象。
js
console.log(Object.prototype.toString.call('Hello, World!')); // 输出'[object String]'
console.log(Object.prototype.toString.call(3.14)); // 输出'[object Number]'
console.log(Object.prototype.toString.call(true)); // 输出'[object Boolean]'
console.log(Object.prototype.toString.call(undefined)); // 输出'[object Undefined]'
console.log(Object.prototype.toString.call(null)); // 输出'[object Null]'
console.log(Object.prototype.toString.call(Symbol())); // 输出'[object Symbol]'
console.log(Object.prototype.toString.call(123n)); // 输出'[object BigInt]'
console.log(Object.prototype.toString.call({})); // 输出'[object Object]'
console.log(Object.prototype.toString.call([])); // 输出'[object Array]'
console.log(Object.prototype.toString.call(function() {})); // 输出'[object Function]'八、闭包
闭包是指能够访问并记住其词法作用域(定义时所处于的作用域)变量的函数,即使这个函数在作用域之外被执行。
形成条件:
- 函数嵌套
- 内部函数引用外部函数的变量
常见应用:
1.创建私有变量
闭包可以模拟私有变量,这些变量只能通过特定的公共方法进行访问和修改,从而实现数据的封装。
js
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() { count++; return count; },
decrement: function() { count--; return count; },
getCount: function() { return count; }
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
// 无法直接从外部访问 count,例如 counter.count 是 undefined2.函数工厂
闭包可以用于创建函数工厂,即根据不同参数生成不同函数的模式。
js
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15注意事项与潜在问题
1.内存泄漏
由于闭包会长期持有对外部变量的引用,如果不慎引用了不再需要的大对象(如大型数组、DOM元素),可能会导致这些对象无法被垃圾回收,从而引起内存泄漏。在不需要闭包时,可以手动将引用置为 null来辅助垃圾回收。
2.循环中的闭包陷阱
在循环中使用 var声明变量并创建闭包时,可能会遇到一个常见问题:所有闭包都引用同一个循环变量,最终得到的是循环结束后的最终值。
js
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出三个 3,而不是预期的 0, 1, 2
}, 100);
}解决方案:使用 let声明循环变量,因为 let具有块级作用域,每次迭代都会创建一个新的变量绑定,每个闭包捕获的都是当次迭代的 i值。
js
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 正确输出 0, 1, 2
}, 100);
}另一种传统方法是使用**立即执行函数表达式(IIFE)** 为每次迭代创建一个新的作用域。
js
for (var i = 0; i < 3; i++) {
(function(i) {
setTimeout(function() {
console.log(i); // 正确输出 0, 1, 2
}, 100);
})(i); // 将当前 i 的值作为参数传入
}九、原型和原型链
原型是一个对象,它是用来创建其他对象的模板。每个函数都有一个prototype属性,它指向该函数的原型对象。
原型链是由一系列原型对象组成的链状结构。每个对象都有一个__proto__属性,它指向它的原型对象。当访问一个对象的属性或方法时,JavaScript引擎会先在当前对象上查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末尾(null)。
原型链的作用是实现继承。
原型链图示:
person1 --> Person.prototype --> Object.prototype --> null如何检查一个属性是对象自身的还是继承自原型链的?
使用hasOwnProperty()方法:
js
person1.hasOwnProperty('name'); // true
person1.hasOwnProperty('greet'); // false如何实现继承?
通过原型链实现继承:
js
function Person(name) {
this.name = name;
}
function Employee(name, title) {
Person.call(this, name);
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.sayTitle = function () {
console.log("My title is " + this.title);
}如何避免原型链污染?
- 不要直接修改Object.prototype,因为这会影响所有对象。
- 使用Object.freeze()冻结原型。
- 使用Object.create(null)创建无原型的对象。
十、this指向
this的值完全取决于函数的调用方式,而不是定义的位置。
js
// 1.在全局作用域中,this指向全局对象:
console.log(this === window); // 输出true(在浏览器中)
// 2.在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象:
function foo() {
console.log(this === window); // 输出true(在浏览器中)
}
foo();
// 3.在作为对象方法调用时,this指向调用该方法的对象:
let obj = {
myMethod: function() {
console.log(this === obj); // 输出true
}
};
obj.myMethod();
// 4.在构造函数中,this指向新创建的对象:
function MyConstructor() {
this.myProperty = 'Hello World!';
console.log(this instanceof MyConstructor); // 输出true
}
let myInstance = new MyConstructor();
// 5.在事件处理程序中,this指向触发事件的元素:
<button id="myButton">点击!</button>
<script>
let button = document.getElementById('myButton');
button.onclick = function() {
console.log(this === button); // 输出true
};
</script>
// 6.使用call()、apply()和bind()方法显式地设置函数调用时的this值:
function foo() {
console.log(this);
}
let obj = { a: 1 };
foo.call(obj); // 输出{ a: 1 }
foo.apply(obj); // 输出{ a: 1 }
let bar = foo.bind(obj);
bar(); // 输出{ a: 1 }十一、call、apply、bind
用于显式控制函数执行时 this指向。
- call
call方法会立即调用函数,第一个参数指定 this的指向,后续参数以列表形式逐个传递给函数。
js
const person = { name: 'Alice' };
function introduce(greeting, age) {
console.log(`${greeting}, 我是${this.name}, 今年${age}岁。`);
}
// 使用 call,'Hello' 和 25 作为单独参数传递
introduce.call(person, 'Hello', 25); // 输出:Hello, 我是Alice, 今年25岁。[2,7](@ref)- apply
apply方法也会立即调用函数,与 call的关键区别在于它接受一个数组或类数组作为第二个参数,数组元素会展开成为函数的参数。
js
const person = { name: 'Bob' };
function introduce(greeting, age, hobby) {
console.log(`${greeting}, 我是${this.name}, 今年${age}岁,喜欢${hobby}。`);
}
// 参数放在一个数组中传递
const args = ['Hi', 30, '爬山'];
introduce.apply(person, args); // 输出:Hi, 我是Bob, 今年30岁,喜欢爬山。[2,9](@ref)- bind
bind方法不会立即执行函数,而是返回一个全新的函数。这个新函数的 this值被永久绑定到 bind的第一个参数,并且可以预先传入部分参数(这被称为柯里化)。
js
const person = { name: 'Charlie' };
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
// 创建一个新函数boundGreet,其this永久指向person
const boundGreet = greet.bind(person);
// 在未来的某个时刻调用这个新函数
boundGreet('Hey', '!'); // 输出:Hey, Charlie十二、事件循环机制
事件循环(Event Loop)是JavaScript 处理异步任务的核心机制,它确保单线程的 JavaScript 能够高效、非阻塞地运行。
Javascript是单线程的,意味着它只有一个主线程来执行代码。
异步任务的回调函数会被放入不同的队列中等待执行。这些队列主要分为两类:
宏任务队列:包括 setTimeout、setInterval、I/O 操作(如网络请求、文件读写)、UI 渲染(浏览器)、DOM 事件回调等。可以将其视为相对“庞大”的工作单元。
微任务队列: 包括 Promise.then/catch/finally、MutationObserver、queueMicrotask,以及在 Node.js 中优先级极高的 process.nextTick。
详细工作流程
1.执行同步代码
→ 主线程按顺序执行所有同步代码(如 console.log),遇到异步任务(如 setTimeout/Promise)时,将其回调分别丢进 宏任务队列 或 微任务队列,继续执行后续同步代码。
2.清空微任务队列
→ 同步代码执行完后,立即检查微任务队列(如 Promise.then、await后的代码),全部执行完毕(包括执行过程中新产生的微任务),不留一个。
3.渲染(浏览器特有)
→ 微任务清空后,浏览器可能更新页面(重绘/回流),但这一步不是每次循环都会触发。
4.执行一个宏任务
→ 从宏任务队列(如 setTimeout、click事件)中取出 最早的一个任务 执行。
5.循环
→ 重复步骤2-4,直到所有队列清空。
代码示例:
示例一:
js
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
async function async1() {
console.log('async1 start');
await async2();
// async函数会隐式返回一个 Promise。
// await关键字会暂停其后函数的执行,其后的代码会被转换为微任务
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
console.log('script end');
// 输出结果:
script start
async1 start
async2
script end
promise1
promise2
async1 end
setTimeout示例二:
js
console.log('script start'); // 同步代码
setTimeout(() => { // 宏任务1
console.log('setTimeout1');
Promise.resolve().then(() => { // 宏任务1内部的微任务
console.log('promise3');
// 由于 promise3是在宏任务1执行过程中添加的,所以 在宏任务1结束后立即执行。
});
}, 0);
setTimeout(() => { // 宏任务2
console.log('setTimeout2');
}, 0);
Promise.resolve().then(() => { // 微任务1
console.log('promise1');
}).then(() => { // 微任务2
console.log('promise2');
});
console.log('script end'); // 同步代码
// 输出结果:
script start
script end
promise1
promise2
setTimeout1
promise3
setTimeout2十三、垃圾回收机制
JavaScript的垃圾回收(Garbage Collection, GC)机制是其实现自动内存管理的核心,它负责在代码执行过程中自动分配内存,并在内存不再使用时自动回收,从而避免内存泄漏,减轻开发者的负担。
垃圾回收的核心概念是可达性。一个值如果能够通过某种引用链从根对象访问到,它就是可达的,会被视为存活对象;反之则是不可达的,即垃圾。
根对象通常包括:
- 全局对象(如浏览器中的 window 对象、Node.js 中的 global 对象)
- 当前执行上下文中的局部变量和函数参数
- 活动的函数调用栈
- 未被移除的DOM元素的引用等
标记-清除法 回收的过程可以概括为:
- 标记阶段:从根对象开始,遍历所有可达对象,将它们标记为“可达”。
- 清除阶段:遍历所有对象,将未被标记为“可达”的对象(即垃圾)清除。
引用-计数法 回收的过程可以概括为:
跟踪每个对象被引用的次数,当引用数为0时立即回收。
常见的内存泄漏
1. 意外的全局变量
在函数中忘记使用 var、let 或 const 声明变量,导致它成为全局对象的属性。
js
function createGlobal() {
accidentalGlobal = "这个变量会泄漏到window对象上"; // 忘记使用 let/const/var
}
createGlobal();
// 即使函数执行完,accidentalGlobal 仍可通过 window.accidentalGlobal 访问,不会被回收2. 被遗忘的定时器或事件监听器
如果在代码中创建了定时器(如 setTimeout、setInterval)或事件监听器(如 addEventListener),但在适当的时候没有清除它们,就会导致内存泄漏。
js
function startTimer(element) {
setInterval(() => {
// 这个回调函数持有对 element 的引用(即使element已从DOM移除)
if (element.parentNode) {
element.textContent = new Date().toLocaleTimeString();
}
}, 1000);
}
const timerElement = document.getElementById('timer');
startTimer(timerElement);
// 即使从DOM中移除了timerElement,由于定时器回调仍在执行并引用它,该DOM元素占用的内存不会被释放3. 脱离DOM的引用
在JavaScript中保存了对DOM元素的引用,即使该元素已从页面中移除,它也不会被回收。
js
const detachedNodes = [];
function storeElement() {
const listItem = document.createElement('li');
listItem.textContent = 'List Item';
document.body.appendChild(listItem);
detachedNodes.push(listItem); // 在数组中也保留一份引用
document.body.removeChild(listItem); // 从DOM中移除
}
storeElement();
// 此时,listItem 元素虽然不在DOM树中,但仍被 detachedNodes 数组引用,因此不会被GC回收4. 闭包的不当使用
闭包可以维持函数内部变量的引用,如果闭包本身长期存在,其引用的变量也会一直存活。
js
function outer() {
const largeData = new Array(1000000).fill('data'); // 占用大量内存的数据
return function inner() { // 返回一个内部函数(闭包)
// 这个闭包隐式地持有了对 largeData 的引用
console.log('Inner function called');
};
}
const closureFn = outer(); // closureFn 持有对 inner 函数的引用,而 inner 又引用了 largeData
// 即使 outer 执行完毕,只要 closureFn 存在,largeData 就无法被回收十四、isNaN()和Number.isNaN()的区别
isNaN() 函数用于检查一个值是否为 NaN(Not-a-Number)。它会尝试将参数转换为数字,然后检查是否转换失败。如果转换失败,返回 true;否则返回 false。
Number.isNaN() 方法用于检查一个值是否为 NaN,与 isNaN() 不同的是,它不会尝试将参数转换为数字,只有当参数严格等于 NaN 时才返回 true。
示例:
js
isNaN('123'); // false,'123' 可以转换为数字 123
isNaN('abc'); // true,'abc' 不能转换为数字
isNaN(NaN); // true,NaN 是 NaN
isNaN(undefined); // true,undefined 不能转换为数字
Number.isNaN(NaN); // true,NaN 是 NaN
Number.isNaN('123'); // false,'123' 不是 NaN
Number.isNaN(undefined); // false,undefined 不是 NaN