1、Javascript 有多少种数据类型,如何判断?
JavaScript 有两种不同的数据类型集:原始类型和对象类型。
原始类型包括:
- Undefined:一个未赋值的变量会有一个默认值
undefined。 - Null:表示一个空值(注意
typeof null返回"object",这实际上是一个历史遗留的错误)。 - Boolean:有两个值
true和false。 - Number:表示整数和浮点数。
- String:表示文本数据。
- Symbol(ES6 新增):表示唯一的、不可变的数据值,常用作对象属性的键。
- BigInt(ES2020 新增):表示大于
2^53 - 1的整数。
对象类型包括:
- Object:表示一个实例对象,可以包含一组键值对。
判断数据类型的方法:
- 使用
typeof操作符可以判断一个变量是否为原始类型(除了null,typeof null会返回"object")。 - 使用
instanceof操作符可以判断一个对象是否为某个构造函数的实例。 - 使用
Object.prototype.toString.call()方法可以得到对象的类内部属性,这是一个更准确的类型检测方法。
例如:
typeof 42; // "number"
typeof 'JavaScript'; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol('sym'); // "symbol"
typeof BigInt(1234567890123456789012345678901234567890); // "bigint"
typeof null; // "object"(这是一个历史遗留的错误)
typeof {}; // "object"
typeof []; // "object"
typeof function() {}; // "function"(函数在 JavaScript 中是对象的一种特殊类型)
// 更准确的类型检测
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(new Date()); // "[object Date]"
注意,虽然函数在 JavaScript 中被视为对象,但 typeof 对函数返回的是 "function"。
2、为什么说 Symbol 是基本数据类型?
Symbol 被认为是基本数据类型(也称为原始数据类型),因为它满足基本数据类型的特征:
- 不可变性:
Symbol的值是不可变的,一旦创建,就不能修改。 - 唯一性:每个
Symbol的值都是唯一的,即使它们有相同的描述也不相等。 - 不可构造:
Symbol不是对象,不能使用new关键字来构造,它是通过调用Symbol函数来创建的。
由于这些特性,Symbol 类型的值可以用作对象属性的键,而不必担心属性名冲突。这使得 Symbol 成为一种适合用作唯一标识符的基本数据类型。
3、JavaScript中基本数据类型有哪些特征?
JavaScript 中的基本数据类型(原始数据类型)具有以下特征:
- 不可变性:基本数据类型的值是不可变的。当对这种类型的值进行操作时,实际上是创建了一个新值。
- 按值传递:基本数据类型的值在赋值或传递给函数时,都是按值传递的。这意味着会创建一个副本。
- 没有属性或方法:基本数据类型本身不是对象,因此没有可以调用的属性或方法。但是,JavaScript 会自动将基本数据类型包装成对象,以便可以调用方法,如字符串的
.length属性或.toUpperCase()方法。 - 存储位置:基本数据类型的值通常存储在栈内存中,这使得访问这些值非常快。
基本数据类型包括:Undefined、Null、Boolean、Number、String、Symbol 和 BigInt。
4、基本类型为什么也可以调方法,比如.toFixed() ?
尽管基本数据类型(如 Number 和 String)本身不是对象,因此没有方法,但 JavaScript 在需要时会临时将它们转换(或称为“包装”)为对象,以便可以调用方法。这个过程通常被称为“自动装箱”。
例如,当你调用一个数值的 .toFixed() 方法时,JavaScript 会临时创建一个 Number 对象,让你能够使用 Number 对象提供的方法。调用方法后,这个临时对象就会被丢弃,你继续持有的是原始的基本类型值。
这个过程是透明的,所以看起来就像基本类型值本身就有方法可以调用。实际上,这些方法是定义在原始类型对应的原型对象上的,例如 Number.prototype.toFixed、String.prototype.toUpperCase 等。
5、如何理解原型链?
原型链是 JavaScript 中对象之间继承属性和方法的一种机制。以下是原型链的基本概念:
- 原型对象:在 JavaScript 中,几乎所有的对象都有一个特殊的内部属性,指向它们的原型对象。这个原型对象本身也可能有自己的原型,以此类推,形成了一个“链”。
- 属性查找:当你尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会查找对象的原型。如果原型对象上也没有这个属性或方法,引擎会继续查找原型的原型,这个过程会一直进行下去,直到找到该属性或方法,或者到达原型链的末端(通常是
Object.prototype,它的原型是null)。 - 原型继承:通过原型链,一个对象可以继承另一个对象的属性和方法。这是 JavaScript 中实现继承的主要方式。
- 构造函数的原型:每个函数在创建时都会有一个
prototype属性,这个属性是一个对象,包含应该由该函数构造的所有实例共享的属性和方法。当使用new关键字调用构造函数创建一个新对象时,这个新对象的内部[[Prototype]]属性会被赋值为构造函数的prototype对象。 - 原型链的末端:原型链的末端是
Object.prototype。由于几乎所有的原型最终都会继承自Object.prototype,这就解释了为什么几乎所有的 JavaScript 对象都可以访问Object.prototype上定义的方法,如toString()或hasOwnProperty()。
通过这种方式,原型链提供了一种共享和继承属性和方法的机制,这是 JavaScript 中面向对象编程的基础。
如何理解闭包?
闭包(Closure)是一个函数和其词法环境的组合。在JavaScript中,当一个函数被创建时,它会保留对创建时作用域中变量的引用。这意味着即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。
闭包的特点:
- 记忆作用域: 内部函数记住了其被创建时的环境。
- 封装私有变量: 可以用来模拟私有变量,这些变量不能直接从外部访问,只能通过闭包提供的函数访问。
- 持久化变量: 闭包可以使得函数中的变量在外部函数执行完毕后不被垃圾回收机制回收,从而实现数据的持久化。
例子:
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
在这个例子中,createCounter函数返回了一个匿名函数,这个匿名函数形成了一个闭包,它可以访问并修改createCounter作用域中的count变量。即使createCounter的执行上下文已经结束,匿名函数仍然可以访问count变量,因为它被闭包所保存。
常⻅的 JS 语法错误类型有哪些?举几个例子。
JavaScript中常见的语法错误类型包括:
-
未声明的变量 - 使用未声明的变量会导致
ReferenceError。console.log(a); // ReferenceError: a is not defined -
拼写错误 - 变量或函数名拼写错误。
var myVarible = "Hello"; console.log(myVariable); // ReferenceError: myVariable is not defined -
缺少分号 - JS引擎虽然会尝试自动插入分号,但有时会导致不预期的结果。
var a = 3 (b = 4).toString(); // TypeError: Cannot read property 'toString' of undefined -
括号、花括号、方括号不匹配 - 括号未正确关闭。
if (a > 1) { console.log(a); // SyntaxError: missing } after function body -
不正确的使用关键字 - 如将
if、for等关键字用作变量名。var if = 5; // SyntaxError: Unexpected token if -
逗号和点的错误使用 - 错误地使用逗号或点。
var obj = {a: 1, b: 2,}; // 尾随逗号在ES5之前是错误的 console.log(obj.,a); // SyntaxError: Unexpected token , -
字符串未闭合 - 字符串字面量未正确闭合。
var str = 'Hello; // SyntaxError: Invalid or unexpected token -
赋值错误 - 使用等号
=代替比较运算符==或===。if (a = 3) { // 这里实际上是赋值操作,而不是比较操作 } -
使用未定义的函数 - 调用不存在的函数。
myFunction(); // ReferenceError: myFunction is not defined -
数字字面量格式错误 - 数字格式不正确。
var num = 0123; // 在严格模式下,八进制字面量是不允许的
这些错误通常会在代码执行时被JavaScript引擎抛出,并且大多数现代开发环境和编辑器都会提供语法检查来帮助开发者在代码运行前发现这些错误。
11、在 try-catch 或 with 中定义变量有什么影响?
在try-catch或with语句中定义变量会影响变量的作用域和提升行为。
-
try-catch: 当在
try块中定义变量时,这些变量的作用域被限制在try块内。如果在catch块中定义变量,那么这些变量的作用域被限制在catch块内。然而,由于JavaScript的变量提升(hoisting)特性,如果使用var关键字声明变量,这些变量会被提升到函数或全局作用域的顶部,但是它们的赋值仍然会在try-catch块内进行。使用let或const声明的变量则会具有块级作用域,不会被提升。try { var a = 1; // a 被提升到函数或全局作用域顶部 let b = 2; // b 的作用域限制在 try 块内 } catch (e) { var c = 3; // c 被提升到函数或全局作用域顶部 let d = 4; // d 的作用域限制在 catch 块内 } console.log(a); // 输出 1 console.log(c); // 输出 undefined,因为没有错误被捕获 // console.log(b); // ReferenceError: b is not defined // console.log(d); // ReferenceError: d is not defined -
with:
with语句会创建自己的作用域,其中定义的变量可以是现有对象的属性或新的全局变量(如果对象没有相应的属性)。由于with语句会改变正常的作用域链,它通常被认为是不推荐使用的,因为它可能导致代码难以理解和调试。var obj = {a: 1}; with (obj) { a = 2; // 修改了 obj 的属性 a b = 3; // 如果 obj 没有 b 属性,这会创建一个全局变量 b } console.log(obj.a); // 输出 2 console.log(b); // 输出 3,假设 obj 没有 b 属性
由于with语句可能导致潜在的问题,且在严格模式下是禁用的,建议避免使用with。而在使用try-catch时,最好使用let和const来声明变量,以避免变量提升带来的混淆。
12、JS 中如何确定 this 的值?
在JavaScript中,this的值取决于函数的调用方式,而不是定义方式。以下是确定this值的几种常见情况:
-
全局上下文: 在全局执行上下文中(非严格模式),
this指向全局对象;在浏览器中是window对象,在Node.js中是global对象。在严格模式下,this的值为undefined。 -
函数调用: 当函数作为普通函数调用时(即不作为对象的方法调用),
this通常指向全局对象(非严格模式)或undefined(严格模式)。 -
方法调用: 当函数作为对象的方法调用时,
this指向该方法所属的对象。 -
构造函数: 在构造函数中,
this指向新创建的对象实例。 -
箭头函数: 箭头函数没有自己的
this,它会捕获其所在上下文的this值。 -
DOM事件处理器: 当函数作为DOM事件处理函数时,
this指向触发事件的元素。 -
显式绑定: 使用
.call()、.apply()或.bind()方法可以显式地设置this的值。
示例:
function showThis() {
console.log(this);
}
showThis(); // 全局对象或undefined(严格模式)
const obj = {
method: function() {
console.log(this);
}
};
obj.method(); // obj
function Constructor() {
console.log(this);
}
new Constructor(); // Constructor的一个新实例
const arrowFunc = () => console.log(this);
arrowFunc(); // 箭头函数外部的this值
document.body.addEventListener('click', function() {
console.log(this); // document.body
});
showThis.call(obj); // obj
showThis.apply(obj); // obj
const boundFunc = showThis.bind(obj);
boundFunc(); // obj
理解this的值是如何确定的,对于编写可预测的JavaScript代码非常重要。
13、实现 apply, call, bind
要在JavaScript中实现apply、call和bind函数,你需要理解它们的基本原理:
call和apply都是用来调用函数的,它们的区别在于传递参数的方式:call需要将参数依次传递,而apply则是将所有参数作为数组传递。bind则是创建一个新的函数,你可以将一个this值和若干个指定的参数传递给这个新函数。
以下是这三个函数的简单实现:
// 实现 call
Function.prototype.myCall = function(context = window, ...args) {
if (this === Function.prototype) {
return undefined; // 防止 Function.prototype.myCall() 直接调用
}
context = context || window;
const fnSymbol = Symbol(); // 声明一个独有的Symbol属性,防止fn覆盖已有属性
context[fnSymbol] = this; // this指向调用call的函数
const result = context[fnSymbol](...args); // 执行函数
delete context[fnSymbol]; // 删除属性
return result; // 返回函数执行结果
};
// 实现 apply
Function.prototype.myApply = function(context = window, args = []) {
if (this === Function.prototype) {
return undefined; // 防止 Function.prototype.myApply() 直接调用
}
context = context || window;
const fnSymbol = Symbol();
context[fnSymbol] = this;
let result;
if (Array.isArray(args)) {
result = context[fnSymbol](...args);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
// 实现 bind
Function.prototype.myBind = function(context, ...args1) {
if (this === Function.prototype) {
throw new TypeError('Error');
}
const _this = this;
return function F(...args2) {
// 判断是否用于构造函数
if (this instanceof F) {
return new _this(...args1, ...args2);
}
return _this.apply(context, args1.concat(args2));
};
};
在这些实现中,我们使用了Symbol来创建一个唯一的属性,以避免与context对象上已有的属性冲突。对于bind的实现,我们还需要处理返回的函数被作为构造函数使用的情况。