一、bind 连续执行多次后的 this 指向
在 JavaScript 中,可以使用 bind() 方法来改变函数执行时的 this 上下文。
如果连续多次对同一个函数使用 bind() 方法,那么每次调用 bind() 方法都会返回一个新的函数,但是这些新函数的 this 上下文都是相同的,指向第一个绑定的对象。
根据 ECMAScript 规范,bind() 方法返回一个"绑定函数"(Bound Function),这个绑定函数有内部槽:
[[BoundTargetFunction]]:原始函数[[BoundThis]]:绑定的 this 值
关键特性:一旦使用 bind() 绑定了 this,后续的 bind() 调用无法改变已经绑定的 this。
function greet() {
console.log(this.name);
}
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
// 第一次绑定
const bound1 = greet.bind(obj1);
bound1(); // 输出: obj1
// 第二次绑定(无效)
const bound2 = bound1.bind(obj2);
bound2(); // 仍然输出: obj1(不是 obj2)
// 绑定 null 的情况
const bound3 = greet.bind(null);
bound3(); // 输出: undefined(严格模式)或全局对象的 name 属性
// 对绑定 null 的函数再次绑定
const bound4 = bound3.bind(obj2);
bound4(); // 输出: undefined(仍然无法改变)
注意:如果在使用 bind() 方法时,最终绑定的对象为 null 或 undefined,那么 this 上下文,在非严格模式下会指向全局对象 window(浏览器环境)或 global(Node.js 环境),在严格模式下保持为 null 或 undefined,这可能会导致不可预期的行为。
二、0.1+0.2 为什么不等于 0.3
在 JavaScript 中,浮点数是以 IEEE 754 标准的二进制浮点数表示的,它采用二进制的形式来表示实数。而二进制无法精确地表示某些十进制小数,例如 0.1 和 0.2,因为它们在二进制下是无限循环的小数,而浮点数只有 64 位的精度。因此,当在 JavaScript 中执行 0.1 + 0.2 的计算时,由于无法精确表示这两个数字,它们会被转换成最接近的可表示二进制数,然后再进行计算。这会导致一个微小的舍入误差,使得结果不等于 0.3。
2.1 解决办法
2.1.1 使用容差范围比较
// 设置一个极小的误差范围(容差)
const EPSILON = 1e-10;
function areEqual(a, b) {
return Math.abs(a - b) < EPSILON;
}
console.log(areEqual(0.1 + 0.2, 0.3)); // true
// 使用Number.EPSILON(ES6+)
function areEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(areEqual(0.1 + 0.2, 0.3)); // true
2.1.2 转换为整数计算
// 将小数转换为整数进行计算,再转回小数
function addDecimals(a, b) {
const multiplier = Math.pow(10, Math.max(
a.toString().split('.')[1]?.length || 0,
b.toString().split('.')[1]?.length || 0
));
return (a * multiplier + b * multiplier) / multiplier;
}
console.log(addDecimals(0.1, 0.2) === 0.3); // true
2.1.3 四舍五入处理
可以使用 toFixed() 将结果四舍五入到指定小数位数,需要注意的是,toFixed() 方法返回一个字符串类型的结果,因此需要注意类型转换。
const result = (0.1 + 0.2).toFixed(1);
console.log(parseFloat(result) === 0.3); // true
2.1.4 使用 decimal.js、big.js 等专门处理精度的库
import Decimal from 'decimal.js';
const sum = new Decimal(0.1).plus(0.2);
console.log(sum.equals(0.3)); // true
console.log(sum.toNumber()); // 0.3
三、Symbol 的作用
Symbol 是在 ES6 新增的基础数据类型,它的主要作用是创建一个唯一的标识符,用于对象属性名的命名、常量的定义等场景。
每个 Symbol 都是唯一的,可以用作对象的属性名,这样可以避免属性名冲突的问题。
const s1 = Symbol();
const s2 = Symbol();
const obj = {
[s1]: 'hello',
[s2]: 'world'
};
Symbol 可以用于实现一些常量或枚举值,这些值是不可修改和重复的。
const Colors = {
Red: Symbol('Red'),
Green: Symbol('Green'),
Blue: Symbol('Blue')
};
console.log(Colors.Red); // Symbol(Red)
由于 Symbol 是一种基础数据类型,所以它具有很高的性能和可靠性,可以用于需要创建和使用高效和安全的标识符的场景。
注意:Symbol 还有一个重要的特点是,它不会出现在 for...in、for...of、Object.keys()、Object.getOwnPropertyNames() 等遍历对象属性的方法中,因此可以用来定义一些不希望被遍历到的属性,例如一些内部实现细节或隐藏属性。但可以使用 Object.getOwnPropertySymbols() 或 Reflect.ownKeys() 获取到 Symbol 类型的属性名。
四、typeof null 为什么是 object
typeof null 的结果为 "object",这是 JavaScript 语言的一个历史遗留问题。在 JavaScript 最初的版本中,使用 32 位的值表示一个变量,其中前 3 位用于表示值的类型。000 表示对象,010 表示浮点数,100 表示字符串,110 表示布尔值,和其他的值都被认为是指针。在这种表示法下,null 被解释为一个全零的指针,也就是说它被认为是一个空对象引用,因此 typeof null 的结果就是 "object"。
在判断变量是否为 null 时,建议使用严格相等运算符(===)进行判断。
五、ES6 的新特性
ES6(ECMAScript 2015)是 JavaScript 的一个新版本,引入了很多新的特性和语法,其中一些比较常用的包括:
- 块级作用域:通过
let和const声明的变量只在当前块级作用域中有效。 - 箭头函数:使用
=>符号定义的函数,具有简化的语法和自动绑定 this 上下文的特点。 - 模板字符串:使用反引号
`和${}操作符,可以方便地拼接字符串和变量。 - 解构赋值:可以将数组或对象的值解构赋给变量。
- 类和继承:引入了
class和extends关键字,使得 JavaScript 支持面向对象编程。 - Promise 和 async/await:用于处理异步编程的新特性。
六、箭头函数和普通函数有何区别?
关于箭头函数和普通函数的区别,主要有以下几点:
6.1 this 上下文
箭头函数没有自己的 this 上下文,它的 this 上下文继承自外部作用域,因此不能使用 call()、apply() 或 bind() 方法改变 this 上下文。
6.2 arguments 对象
箭头函数没有自己的 arguments 对象,如果需要获取函数参数,可以使用 rest 参数或者展开运算符。
箭头函数没有自己的 arguments
const arrowFunc = (a, b) => {
console.log('箭头函数的 arguments:', arguments); // 这里访问的是外层作用域的 arguments
console.log('a:', a, 'b:', b);
}
arrowFunc(1, 2, 3, 4);
// 在严格模式下会报错:ReferenceError: arguments is not defined
箭头函数继承外层函数的 arguments
function outerFunction() {
console.log('外层的 arguments:', arguments); // [10, 20, 30]
const innerArrow = () => {
console.log('箭头函数内部的 arguments:', arguments); // 继承外层的 arguments
};
innerArrow(40, 50, 60); // 这里的参数不会被箭头函数自己的 arguments 接收
}
outerFunction(10, 20, 30);
// 外层的 arguments: Arguments(3) [10, 20, 30]
// 箭头函数内部的 arguments: Arguments(3) [10, 20, 30]
使用 rest 参数获取所有参数
const arrowFuncWithRest = (...args) => {
console.log('使用 rest 参数:', args);
console.log('参数个数:', args.length);
console.log('第一个参数:', args[0]);
// 可以像数组一样使用
args.forEach((arg, index) => {
console.log(`参数 ${index}:`, arg);
});
};
arrowFuncWithRest(1, 2, 3, 4, 5);
rest 参数与展开运算符配合使用
const maxValue = (...values) => {
return Math.max(...values); // 展开运算符
};
const numbers = [10, 5, 8, 20, 3];
console.log('最大值:', maxValue(...numbers)); // 20
6.3 构造函数
箭头函数不能作为构造函数使用,不能使用 new 关键字创建对象。
七、箭头函数能当构造函数吗?
根据规范来说,箭头函数是没有 [[Construct]] 方法的,因此不能使用 new 关键字创建对象。如果强制使用 new 关键字调用箭头函数,会抛出一个类型错误 TypeError: xxx is not a constructor。
因此,一般来说箭头函数不应该用于创建对象,而应该用于函数式编程和简化回调函数等场景。
八、Promise.all 和 Promise.allSettled 的区别
Promise.all() 和 Promise.allSettled() 是两个 JavaScript Promise 相关的方法,它们都用于处理多个 Promise 对象的并发执行。
8.1 区别
8.1.1 Promise.all() 方法
返回的 Promise 对象在所有的 Promise 对象都 resolve 之后,才会 resolve 并返回一个由所有 Promise 返回值组成的数组。如果其中有一个 Promise 被 reject,则会立即 reject 并返回相应的错误信息。
简单来说:只有所有 Promise 都成功了才算成功,有一个失败了就算失败。
8.1.2 Promise.allSettled() 方法
返回的 Promise 对象在所有的 Promise 对象都 resolve 或 reject 之后,才会 resolve 并返回一个由所有 Promise 状态对象组成的数组,每个状态对象包含一个 status 字段表示 Promise 状态,和一个 value 或 reason 字段表示 Promise 的返回值或错误信息。
简单来说:它会等到所有 Promise 都执行完毕,无论成功还是失败,都会把每个 Promise 的状态信息收集到一个数组里面返回。
8.2 总结
Promise.all() 方法在需要所有 Promise 都成功的情况下使用,而 Promise.allSettled() 方法则适用于需要知道每个 Promise 的执行结果的情况下使用。
8.3 补充:Promise.race() 和 Promise.any()
8.3.1 Promise.race() 方法
返回的 Promise 对象在第一个 Promise 对象 resolve 或 reject 后,就会立即 resolve 或 reject,并返回第一个完成的 Promise 的结果。
简单来说:谁先完成就返回谁的结果,无论成功还是失败。
const promise1 = new Promise((resolve) => setTimeout(resolve, 500, 'one'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'two'));
Promise.race([promise1, promise2]).then((value) => {
console.log(value); // 输出: "two"(因为 promise2 先完成)
});
8.3.2 Promise.any() 方法
返回的 Promise 对象在第一个 Promise 对象 resolve 后,就会立即 resolve 并返回第一个成功的 Promise 的结果。只有当所有 Promise 都 reject 时,才会 reject 并返回一个 AggregateError 错误。
简单来说:只要有一个成功就算成功,全部失败才算失败。
const promise1 = Promise.reject('error1');
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'success'));
const promise3 = Promise.reject('error3');
Promise.any([promise1, promise2, promise3]).then((value) => {
console.log(value); // 输出: "success"
}).catch((error) => {
console.log(error); // 只有全部失败才会执行
});
四种 Promise 方法的对比总结:
Promise.all():全部成功才成功,一个失败就失败Promise.allSettled():等待所有完成,返回所有结果Promise.race():第一个完成就返回(无论成功失败)Promise.any():第一个成功就成功,全部失败才失败
九、判断数据类型的方法
9.1 方法一:使用 typeof 操作符
typeof 操作符可以返回一个值的数据类型,它适用于除了 null 以外的所有值,因为 typeof null 返回 "object"。
记得返回的字符串是小写的,跟后面的 Object.prototype.toString 的不一样。
9.2 方法二:使用 instanceof 操作符
instanceof 操作符可以判断一个对象是否是某个构造函数的实例。
object instanceof constructor
9.3 方法三:使用 Object.prototype.toString 方法
可以返回一个值的内部类型,它适用于所有值,包括 null 和 undefined。
注意:使用 Object.prototype.toString 方法判断基本类型值时,返回的是其包装对象的类型,而不是基本类型本身的类型。
Object.prototype.toString.call(value)
返回值是一个形如 [object Type] 的字符串,其中 Type 表示值的内部类型。
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(123); // "[object Number]"
Object.prototype.toString.call("abc"); // "[object String]"
Object.prototype.toString.call(Symbol("foo")); // "[object Symbol]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(function () {}); // "[object Function]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(/abc/); // "[object RegExp]"
9.4 方法四:特定类型的判断
Array.isArray 判断数组类型、isNaN 判断是否为 NaN、Number.isInteger 判断是否为整数等。
9.5 方法五:使用 Object.is() 方法
Object.is() 方法用于判断两个值是否为同一个值,它与 === 的主要区别在于对 NaN 和 -0 的处理:
// Object.is() 与 === 的区别
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(-0 === +0); // true
console.log(Object.is(-0, +0)); // false
// 其他情况与 === 相同
console.log(Object.is(1, 1)); // true
console.log(Object.is('foo', 'foo')); // true
console.log(Object.is(null, null)); // true
console.log(Object.is(undefined, undefined)); // true
使用场景:当需要精确判断 NaN 或区分 -0 和 +0 时,使用 Object.is()。
不同的判断方式适用于不同的场景,具体选择哪一种方式应根据实际情况而定。
十、substring 和 substr 的区别
substring() 和 substr() 都是 JavaScript 字符串的方法,用于截取字符串的一部分。
10.1 区别在于参数的不同
10.1.1 substring() 方法
接收两个参数,起始位置和结束位置。它截取从起始位置到结束位置之间的字符,包括起始位置的字符,但不包括结束位置的字符。如果省略第二个参数,则截取到字符串末尾。
10.1.2 substr() 方法
接收两个参数,起始位置和截取的字符数。它从起始位置开始截取指定数量的字符,如果省略第二个参数,则截取到字符串末尾。
注意:substr() 方法被普遍建议弃用,虽然它未被完全从标准中移除,但已被标记为遗留函数,并可能在未来被删除。
十一、== 和 === 的区别
11.1 == 运算符
进行比较时,会先进行类型转换,然后再比较两个值是否相等。
类型转换的规则比较复杂,但可以简单地概括为以下几点:
- 如果两个值类型相同,则直接比较它们的值。
- 如果一个值是 null,另一个值是 undefined,则它们相等。
- 如果一个值是数字,另一个值是字符串,则将字符串转换为数字后再比较。
- 如果一个值是布尔值,另一个值是非布尔值,则将布尔值转换为数字后再比较。
- 如果一个值是对象,另一个值是数字、字符串或布尔值,则将对象转换为原始值后再比较。
11.2 === 运算符
进行比较时,不进行类型转换,只有当两个值的类型和值都相等时才会返回 true。
11.3 总结
一般来说,建议优先使用 === 运算符进行比较,因为它可以避免类型转换的问题,更加严格和安全。
十二、异步加载脚本如何实现?
在 Web 应用中,JavaScript 脚本的异步加载可以通过以下方式来实现:
12.1 动态创建 script 标签
动态创建 <script> 标签,并设置其 src 属性为需要加载的脚本 URL。这种方式可以通过设置 onload 或 onreadystatechange 事件来检测脚本是否加载完成。
const script = document.createElement('script');
script.src = 'path/to/script.js';
script.onload = function() {
// 脚本加载完成后执行的回调函数
};
document.body.appendChild(script);
12.2 使用 XMLHttpRequest 或 Fetch API
使用 XMLHttpRequest 对象或 Fetch API 发送异步请求,并在请求成功后将响应文本解析为 JavaScript 代码,然后使用 eval() 函数或 Function() 构造函数来执行脚本。
const xhr = new XMLHttpRequest();
xhr.open('GET', 'path/to/script.js');
xhr.onload = function() {
const script = document.createElement('script');
script.textContent = xhr.responseText;
document.head.appendChild(script);
};
xhr.send();
12.3 使用 async 或 defer 属性
在 script 标签中,使用 async 或 defer 属性,浏览器会异步下载。
- async:不保证执行顺序,下载完就执行
- defer:下载后在 DOM 解析完成后、DOMContentLoaded 事件触发前,按 defer 脚本声明的顺序执行
十三、异步加载脚本与同步加载脚本的区别
相比于同步加载脚本,异步加载具有以下区别:
- 异步加载可以提高页面的加载速度和响应性能,避免因 JavaScript 阻塞而造成页面卡顿的情况。
- 异步加载可以避免因加载脚本而造成的阻塞情况,使页面的其他资源可以更快地加载和呈现。
- 异步加载可以更灵活地控制脚本的加载顺序和执行时间,可以根据页面需要动态加载和卸载脚本,提高页面的可维护性和可扩展性。
function loadScript(src, callback) {
const script = document.createElement('script');
script.src = src;
script.onload = () => callback && callback(null, script);
script.onerror = () => callback && callback(new Error(`加载失败: ${src}`));
document.head.appendChild(script);
return script; // 返回 script 元素,便于后续移除
}
function unloadScript(scriptElement) {
if (scriptElement && scriptElement.parentNode) {
scriptElement.parentNode.removeChild(scriptElement);
console.log('脚本标签已从 DOM 中移除');
}
}
// 注意,移除标签不会撤销脚本执行时定义的全局变量或函数,
// 如果希望彻底"卸载"脚本的影响,需要配合模块化设计,使用脚本提供的清理方法
十四、for in/for of的区别
14.1 for...in
循环用于遍历对象的可枚举属性,它会将对象的每个属性名称(或键名)作为迭代变量来遍历。
注意:for...in 循环遍历的是对象的可枚举属性,包括自有属性和继承属性。因此,它并不适用于遍历数组和类数组对象。
14.2 for...of
循环用于遍历可迭代对象的元素,它会将对象的每个元素作为迭代变量来遍历。可以用于如数组、字符串、Set、Map 等。
注意:for...of 循环只能遍历实现了迭代器接口(Iterator)的对象,因此它不适用于普通的对象。此外,它遍历的是对象的元素值,而不是键名或属性名。
14.3 总结
for...in 适用于遍历对象的属性名,而 for...of 适用于遍历数组、字符串等可迭代对象的元素值。
十五、splice 和 slice 会改变原数组吗?
15.1 splice() 方法
可以在数组中添加、删除或替换元素,并返回被删除的元素,它会改变原数组。
15.2 slice() 方法
是从原数组中返回指定开始和结束位置的元素组成的新数组,它不会改变原数组。
十六、怎么删除数组最后一个元素?
16.1 方法一:使用 pop() 方法
删除并返回数组的最后一个元素
arr.pop();
16.2 方法二:使用 splice() 方法
删除数组的最后一个元素
arr.splice(-1, 1);
16.3 方法三:使用 slice() 方法
创建一个新的数组,包含除了最后一个元素以外的所有元素
const newArr = arr.slice(0, -1);
十七、数组 forEach 能否结束循环?
数组的 forEach 方法默认不支持提前结束循环,即无法使用类似于 break 或 return 的语法来跳出循环。
但是可以使用抛出异常的方式来达到提前结束循环的效果。需要注意的是,这种方式并不常用,通常可以使用 for 循环或 some、every 等数组方法来替代。
十八、合并对象的方法
18.1 方法一:使用 Object.assign()
const mergedObj = Object.assign({}, obj1, obj2);
18.2 方法二:使用扩展运算符
const mergedObj = { ...obj1, ...obj2 };
十九、判断一个对象是不是空对象的方法
19.1 方法一:使用 Object.keys()
使用 Object.keys() 方法获取对象的属性列表,然后判断列表长度是否为0。性能最快。
Object.keys(obj).length === 0
19.2 方法二:使用 JSON.stringify()
使用 JSON.stringify(obj) === '{}' 来判断。性能最慢。
19.3 方法三:使用 Object.getOwnPropertyNames()
包括了不可枚举属性。
Object.getOwnPropertyNames(obj).length === 0
19.4 方法四:使用 for...in 循环
const obj = {};
let isEmpty = true;
for (const prop in obj) {
// for...in 循环会遍历对象原型链上的所有可枚举属性,而不仅仅是对象自身的属性
if (obj.hasOwnProperty(key)) {
isEmpty = false;
break;
}
}
if (isEmpty) {
console.log('obj is empty');
}
对象有可能重写了 hasOwnProperty 方法,更安全的方式是使用 Object.prototype.hasOwnProperty
function isEmptyObject(obj) {
if (obj == null || typeof obj !== 'object') {
return false;
}
for (let key in obj) {
// 使用更安全的检查方式
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
19.5 方法五:使用 Reflect.ownKeys()
包括了 Symbol 属性。
Reflect.ownKeys(obj).length === 0
19.6 完整健壮的解决方案
function isEmptyObject(obj) {
// 1. 检查是否为null或undefined
if (obj == null) return false;
// 2. 检查是否为对象类型
if (typeof obj !== 'object' || Array.isArray(obj)) {
return false;
}
// 3. 检查是否为空对象
return Object.keys(obj).length === 0;
}
二十、requestAnimationFrame 和 requestIdleCallback 的作用
requestAnimationFrame 和 requestIdleCallback 都是用于在浏览器中执行动画或其他高性能任务的 API。
20.1 requestAnimationFrame
浏览器提供的一种动画帧请求机制,会在浏览器下一次绘制之前执行指定的回调函数。
这样做的好处是可以在浏览器下一次绘制时,让浏览器自动完成一些复杂的计算和渲染工作,从而避免了浏览器在短时间内重复执行相同的任务。
使用 requestAnimationFrame 可以实现更加流畅的动画效果,同时也可以减少页面的闪烁和卡顿。
function animate() {
// 在这里编写动画逻辑
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
20.2 requestIdleCallback
一个相对较新的 API,它的作用是在浏览器空闲时执行指定的回调函数。
让开发者能够在浏览器空闲时,进行一些比较耗时的任务,例如计算和渲染。
这样做的好处是可以提高网页的性能和响应速度,同时也可以避免阻塞浏览器的主线程导致用户体验不佳。
注意的是,requestIdleCallback 的回调函数接受一个 IdleDeadline 参数,它包含了当前空闲时间的相关信息。开发者可以通过这个参数,根据浏览器的空闲时间进行任务调度和优化。
function doWork(deadline) {
while (deadline.timeRemaining() > 0) {
// 在这里编写任务逻辑
}
if (还有任务需要执行) {
requestIdleCallback(doWork);
}
}
requestIdleCallback(doWork);
20.3 总结
requestAnimationFrame 适用于需要在下一次绘制之前执行的动画任务,而 requestIdleCallback 则适用于需要在浏览器空闲时执行的耗时任务。
二十一、canvas 怎么判断点在图形内?
判断一个点是否在 Canvas 中的图形内,通常需要使用 Canvas 提供的 API 或数学公式来实现。
21.1 对于简单的图形
如矩形、圆形等,可以使用 Canvas API 中的 isPointInPath 方法进行判断:
21.1.1 判断点是否在矩形内
使用 ctx.rect() 绘制矩形,再使用 ctx.isPointInPath(x, y) 方法判断点是否在矩形内。
21.1.2 判断点是否在圆形内
使用 ctx.arc() 绘制圆形,再使用 ctx.isPointInPath(x, y) 方法判断点是否在圆形内。
21.2 对于复杂的图形
如多边形、不规则图形等,可以使用数学公式进行判断。
例如,对于一个多边形,可以使用射线法来判断点是否在多边形内:
- 将多边形的每条边与射线相交,计算交点的数量。
- 如果交点的数量为奇数,则点在多边形内部;如果交点的数量为偶数,则点在多边形外部。
总结
本文涵盖了 JavaScript 基础知识的重要知识点,包括:
- this 指向:bind 方法的永久绑定特性
- 浮点数精度:0.1+0.2 问题的解决方案
- ES6 新特性:Symbol、箭头函数、Promise 等
- Promise 方法:all、allSettled、race、any 的对比
- 数据类型判断:typeof、instanceof、Object.prototype.toString、Object.is 等多种方法
- 字符串和数组操作:substring/substr、splice/slice 等方法的区别
- 异步编程:脚本加载、requestAnimationFrame 等
- 对象操作:合并、判断空对象等方法
- Canvas API:判断点在图形内的方法
这些概念是 JavaScript 编程的基础,深入理解它们有助于编写更高效、更可靠的代码。希望本文能对您的 JavaScript 学习和复习有所帮助!
如果您对本文内容有任何疑问或补充,欢迎在评论区交流讨论~