1、JavaScript
数据类型
在 JavaScript 中,数据类型可以分为两大类:基本数据类型(原始数据类型)和引用数据类型。
1.1 基本数据类型
基本数据类型是不可变的,包含以下几种:
-
Number: 表示数字,包括整数和浮点数。
let num = 42;
-
String: 表示字符串,文本数据。
let str = "Hello, World!";
-
Boolean: 表示布尔值,只有两个值:
true
和false
。let isTrue = true;
-
Undefined: 表示未定义的值,变量声明但未赋值时的默认值。
let x; console.log(x); // 输出: undefined
-
Null: 表示空值或无值,表示“无”或“空”。
let emptyValue = null;
-
Symbol: ES6 引入的唯一且不可变的值,通常用于对象属性的唯一标识符。
let sym = Symbol('description');
-
BigInt: 用于表示大于
Number.MAX_SAFE_INTEGER
的整数。let bigIntValue = BigInt(123456789012345678901234567890);
1.2 引用数据类型
引用数据类型是可变的,主要包括:
-
Object: 对象是键值对的集合,可以包含多个值和功能。
let obj = { name: "Alice", age: 25 };
-
Array: 数组是特殊类型的对象,用于存储有序的值。
let arr = [1, 2, 3, 4, 5];
-
Function: 函数也是对象,可以被调用。
function myFunction() { return "Hello!"; }
1.3 总结
JavaScript 的数据类型包括:
- 基本数据类型:
Number, String, Boolean, Undefined, Null, Symbol, BigInt
- 引用数据类型:
Object, Array, Function
2、数据类型检测
在 JavaScript 中,可以使用多种方法来检查数据的类型。以下是一些常用的方法:
2.1 typeof
操作符
typeof
是最常用的方法,可以检查基本数据类型和函数类型。
console.log(typeof 42); // "number"
console.log(typeof "Hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (这是一个历史遗留问题)
console.log(typeof Symbol()); // "symbol"
console.log(typeof BigInt(123)); // "bigint"
console.log(typeof function(){}); // "function"
console.log(typeof []); // "object" (底层设计问题)
typeof null == 'object'
不同类型数据在底层都表示为二进制,在js
中二进制前三位都为0的话会被判断为object
类型,
而null
的二进制表示是全0
,自然前三位也是0
,
所以执行typeof
时会返回object
2.2 instanceof
操作符
instanceof
用于检查对象是否是某个构造函数的实例,适用于引用数据类型。
let arr = [1, 2, 3];
console.log(arr instanceof Array); // true
let obj = {};
console.log(obj instanceof Object); // true
function MyClass() {}
let myInstance = new MyClass();
console.log(myInstance instanceof MyClass); // true
2.3 Array.isArray()
专门用于检查一个值是否为数组。
let arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
let notArr = {};
console.log(Array.isArray(notArr)); // false
2.4 Object.prototype.toString.call()
这种方法可以更准确地检查数据类型,尤其是对于 null
和数组。
console.log(Object.prototype.toString.call(42)); // "[object Number]"
console.log(Object.prototype.toString.call("Hello")); // "[object String]"
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([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"
console.log(Object.prototype.toString.call(Symbol())); // "[object Symbol]"
console.log(Object.prototype.toString.call(BigInt(123))); // "[object BigInt]"
2.5 总结
- 使用
typeof
检查基本数据类型和函数。 - 使用
instanceof
检查对象的构造函数。 - 使用
Array.isArray()
检查数组。 - 使用
Object.prototype.toString.call()
进行更精确的类型检查。
//精确类型检测
function dataTypeof(data){
let type = Object.prototype.toString.call(data);
return type.match(/[A-Z][a-z]*/g)[0];
}
3、严格模式
在 JavaScript 中,严格模式(Strict Mode)是一种通过在代码中添加特定指令来启用的模式,它可以帮助你编写更安全和更高效的代码。使用严格模式可以避免一些常见的错误,并且会使某些不安全的操作抛出错误。
如何启用严格模式
你可以在 JavaScript 文件的顶部或在函数内部启用严格模式。以下是两种常见的方法:
1. 在全局作用域中启用严格模式
在 JavaScript 文件的最顶部添加 "use strict";
:
"use strict";
function myFunction() {
// 这里的代码在严格模式下运行
}
2. 在函数内部启用严格模式
在特定函数内部添加 "use strict";
:
function myFunction() {
"use strict";
// 这里的代码在严格模式下运行
}
严格模式的效果
启用严格模式后,以下行为会受到限制或抛出错误:
- 禁止使用未声明的变量。
- 禁止删除变量、对象或函数的名称。
- 禁止使用
this
指向全局对象(在函数中this
为undefined
)。 - 禁止重复参数名。
- 禁止使用某些保留字(如
eval
和arguments
)。
示例
"use strict";
function example() {
x = 3.14; // 报错:未声明的变量
}
example();
在这个例子中,尝试使用未声明的变量 x
会导致错误,因为严格模式不允许这种行为。
总结
- 使用
"use strict";
来启用严格模式。 - 可以在全局作用域或函数内部启用。
- 严格模式有助于捕获错误并提高代码的安全性。
4、this
绑定规则
主要先讨论普通函数,即使用function
关键字定义的函数。普通函数的this
取决于函数的调用方式(调用时的上下文)
4.1 默认绑定
- 普通函数调用时,如果未绑定任何对象,则默认绑定全局对象(浏览器中即
window
)- 但在严格模式下,默认绑定
undefined
- 但在严格模式下,默认绑定
function fn(){
console.log(this);
}
fn(); // Window {...}
"use strict";
function fn(){
console.log(this);
}
fn(); // undefined
function fn1(){
function fn2(){
console.log(this);
}
fn2();
}
fn1(); // Window {...}
fn2
虽然定义在局部作用域中,但是调用时未绑定任何对象,也会默认绑定window
- 虽然
this
绑定了window
,但是在window
对象上是找不到fn2
函数的, - 函数的可见性只与定义时的作用域有关,
fn2
定义在局部作用域,只能在局部作用域查找到和调用
4.2 隐式绑定
- 当函数赋值到对象中时,函数的
this
指向直接调用它的对象
function fn(){
console.log(this.a);
}
var a = 1;
var obj1 = {a:2, fn};
var obj2 = {a:3, obj1};
fn(); // 1 //this指向window
obj1.fn(); // 2 //this指向obj1
obj2.obj1.fn(); // 2 //this指向obj1
- 当进行函数赋值时,会导致绑定丢失,而应用默认绑定
function fn(){
console.log(this.a);
}
var a = 1;
var obj1 = {a:2, fn};
var fn1 = obj1.fn;
fn1(); // 1 //this指向window
fn1 = obj1.fn
其实只是做了一次地址传值,fn1
就是fn
,fn1 === fn
和直接调用fn()
没区别
4.3 显式绑定
- 使用
call, apply, bind
等方法显示指定一个对象绑定this
function fn(b, c){
console.log(this.a, b, c);
}
var a = 1;
var obj = {a:2};
fn.call(obj,3,4); // 2 3 4
fn.apply(obj,[3,4]); // 2 3 4
let fn1 = fn.bind(obj,3,4);
fn1(); // 2 3 4
let fn2 = fn.bind(obj);
fn2(5,6); // 2 5 6
- 如果绑定对象为
null
或undefined
,call, apply, bind
会忽略这些值,实际还是默认绑定
function fn(){
console.log(this.a);
}
var a = 1;
fn.call(null); // 1
fn.call(undefined); // 1
call
、apply
和 bind
是 JavaScript 中用于改变函数 this
上下文的三种方法。它们的主要区别如下:
1. call
- 用法:
func.call(thisArg, arg1, arg2, ...)
- 功能:调用函数并指定
this
的值,后面可以传入参数。 - 示例:
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const person = { name: 'Alice' };
greet.call(person, 'Hello'); // 输出: "Hello, Alice"
2. apply
- 用法:
func.apply(thisArg, [argsArray])
- 功能:与
call
类似,但参数以数组的形式传入。 - 示例:
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const person = { name: 'Bob' };
greet.apply(person, ['Hi']); // 输出: "Hi, Bob"
3. bind
- 用法:
const boundFunc = func.bind(thisArg, arg1, arg2, ...)
- 功能:返回一个新的函数,
this
被永久绑定到指定的值,参数可以预设。 - 示例:
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const person = { name: 'Charlie' };
const greetCharlie = greet.bind(person);
greetCharlie('Hey'); // 输出: "Hey, Charlie"
4.4 new
绑定
- 使用
new
关键字调用函数,会构造一个新对象,将函数的this
绑定到新对象,并返回这个对象
function fn(a) {
this.a = a;
console.log(this);
}
let obj = new fn(3); // fn {a: 3} //fn变成了一个类
console.log(obj.a); // 3
new
创建对象的流程:
-
- 创建一个新的空对象
-
- 设置新对象的
[[Prototype]]
原型为构造函数的原型
- 设置新对象的
-
- 绑定构造函数的
this
为新对象
- 绑定构造函数的
-
- 执行构造函数,如果构造函数没有返回对象,则将新对象返回
4.5 间接引用
function fn() {
console.log(this.a);
}
var a = 1;
let obj1 = {a:2, fn};
let obj2 = {a:3};
fn(); // 1
obj1.fn(); // 2
(obj2.fn = obj1.fn)(); // 1
obj2.fn(); // 3
- 赋值表达式也是有返回值的,返回值就是要赋的值的原始引用
(obj2.fn = obj1.fn)
的返回值就是fn
函数的引用,(obj2.fn = obj1.fn)()
其实就是在执行fn()
,根据默认绑定原则,this
指向window
obj2.fn === fn
这是成立的,但是就是fn()
和obj2.fn()
的调用方式不同,导致this
绑定不同
4.6 箭头函数的this
箭头函数,()=>{}
即不使用function
关键字定义的无名函数。箭头函数的this
取决于函数定义时的上下文
function fn1() {
console.log(this);
}
let fn2 = ()=>{
console.log(this);
}
fn1(); // Window {...}
fn2(); // Window {...}
- 虽然
fn1()
和fn2()
都会指向window
,但内部原因却不一样 fn1()
是因为调用时未绑定任何对象,默认绑定window
fn2()
是因为定义箭头函数时所在上下文是window
,所以指向window
function fn1() {
let fn2 = ()=>{
console.log(this);
}
fn2();
}
fn1(); // Window {...}
fn2
定义在fn1
的局部作用域中,所以fn2
的this
指向fn1
的this
fn1
因为调用时未绑定任何对象,this
默认绑定window
- 那
fn2
的this
也就指向window
let obj = {
fn1:function(){
console.log(this);
},
fn2:()=>{
console.log(this);
}
}
obj.fn1(); // {fn1: ƒ, fn2: ƒ}
obj.fn2(); // Window {...}
obj.fn1
调用时隐式绑定了obj
,所以this
指向obj
obj.fn2
虽然是在给obj
定义函数,但定义操作还是在全局上下文中执行的,所以this
指向window
let obj = {
fn1:function(){
let fn2 = ()=>{
console.log(this);
}
fn2();
}
}
obj.fn1(); // {fn1: ƒ}
fn2
定义在fn1
的局部作用域中,所以fn2
的this
指向fn1
的this
fn1
因为调用时隐式绑定obj
对象,this
指向obj
- 那
fn2
的this
也就指向obj
5、对象深层用法
5.1 存在性
检测属性是否存在于对象上。
let obj = { a: 1 };
obj.__proto__.b = 2;
'a' in obj; // true
'b' in obj; // true
obj.hasOwnProperty('a'); // true
obj.hasOwnProperty('b'); // false
in
操作符会检查属性是否在对象及其[[Prototype]]
原型链中hasOwnProperty
只会检查属性是否在当前对象中
5.2 Object.defineProperty()
Object.defineProperty()
是 JavaScript 中的一个方法,用于在对象上定义一个新属性,或修改现有属性的特性。它允许你精确控制属性的行为。
语法
Object.defineProperty(obj, prop, descriptor);
obj
:要在其上定义属性的对象。prop
:要定义或修改的属性的名称。descriptor
:一个描述符对象,包含属性的特性。
属性描述符
描述符对象可以是以下几种类型:
-
数据描述符:具有值和可写性。
value
:属性的值。writable
:布尔值,指示属性是否可被修改。enumerable
:布尔值,指示属性是否可被枚举。configurable
:布尔值,指示属性是否可被删除或修改其特性。
-
访问器描述符:具有 getter 和 setter。
get
:一个函数,在获取属性值时调用。set
:一个函数,在设置属性值时调用。enumerable
:布尔值,指示属性是否可被枚举。configurable
:布尔值,指示属性是否可被删除或修改其特性。
示例
定义一个数据属性
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: false, // 不能修改
enumerable: true, // 可枚举
configurable: true // 可配置
});
console.log(obj.name); // 输出: "Alice"
obj.name = 'Bob'; // 无效,因为 writable 为 false
console.log(obj.name); // 仍然输出: "Alice"
定义一个访问器属性
const person = {
firstName: 'John',
lastName: 'Doe'
};
Object.defineProperty(person, 'fullName', {
get: function() {
return `${this.firstName} ${this.lastName}`;
},
set: function(value) {
[this.firstName, this.lastName] = value.split(' ');
},
enumerable: true,
configurable: true
});
console.log(person.fullName); // 输出: "John Doe"
person.fullName = 'Jane Smith';
console.log(person.firstName); // 输出: "Jane"
console.log(person.lastName); // 输出: "Smith"
5.3 枚举
可以在对象中设置属性是否可枚举。
let obj = { };
obj.__proto__.a1 = 3;
Object.defineProperty(obj, 'a', {enumerable:true, value:1});
Object.defineProperty(obj, 'b', {enumerable:false, value:2});
obj.propertyIsEnumerable('a'); // true
obj.propertyIsEnumerable('b'); // false
Object.keys(obj); // ['a']
Object.getOwnPropertyNames(obj);// ['a','b']
propertyIsEnumerable
检查属性是否存在于对象中(而不是原型链上),并满足enumerable 为 true
Object.keys(obj)
返回一个包含对象上所有可枚举属性的数组(不包含原型链上的属性)Object.getOwnPropertyNames(obj)
返回一个包含对象上所有属性的数组(不包含原型链上的属性)
5.4 遍历
for...in
可以遍历对象的所有可枚举属性(包括原型链上的)
let obj = { };
obj.__proto__.a1 = 3;
Object.defineProperty(obj, 'a', {enumerable:true, value:1});
Object.defineProperty(obj, 'b', {enumerable:false, value:2});
for(let key in obj){
console.log(key);
}
// a a1
for...of
可以遍历可迭代对象
- 可以遍历可迭代对象(
iterable objects
) - 可迭代对象:包括数组、字符串、Set、Map、Typed Arrays、NodeList 等
Typed Arrays
,类型化数组,比如Uint8Array
、Float32Array
NodeList
,比如document.querySelectorAll
返回的元素数组- 对象本身没有迭代器,但可以自定义迭代器
Symbol.iterator
自定义遍历对象上所有属性(包括原型链)
let obj = { };
obj.__proto__.a1 = 3;
Object.defineProperty(obj, 'a', {enumerable:true, value:1});
Object.defineProperty(obj, 'b', {enumerable:false, value:2});
function getAllProperties(obj) {
let props = [];
while (obj) {
props = props.concat(Object.getOwnPropertyNames(obj));
obj = Object.getPrototypeOf(obj);
}
return props;
}
console.log(getAllProperties(obj));
// [ "a", "b", "a1", "constructor", "__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable", "toString", "valueOf", "__proto__", "toLocaleString" ]
6、浅拷贝与深拷贝
- 浅拷贝:只复制对象的第一层属性,引用类型的属性共享同一引用。
- 深拷贝:递归复制所有层级的属性,引用类型的属性创建全新副本。
浅拷贝
const original = {
a: 1,
b: { c: 2 }
};
// 浅拷贝
const shallowCopy = Object.assign({}, original);
// 修改浅拷贝的引用类型属性
shallowCopy.b.c = 3;
console.log(original.b.c); // 输出: 3 (原始对象也被修改)
深拷贝
JSON.parse(JSON.stringify())
可以复制一些简单类型的值,但无法复制函数、undefined
、Symbol
、BigInt
等
const original = {
a: 1,
b: { c: 2 }
};
// 深拷贝
const deepCopy = JSON.parse(JSON.stringify(original));
// 修改深拷贝的引用类型属性
deepCopy.b.c = 3;
console.log(original.b.c); // 输出: 2 (原始对象未被修改)
- 自定义深拷贝函数,包含所有数据类型
function deepClone(value) {
// 处理基本数据类型
if (value === null || typeof value !== 'object') {
return value; // 基本数据类型直接返回
}
// 处理日期
if (value instanceof Date) {
return new Date(value);
}
// 处理正则表达式
if (value instanceof RegExp) {
return new RegExp(value);
}
// 处理数组
if (Array.isArray(value)) {
return value.map(item => deepClone(item));
}
// 处理 Map
if (value instanceof Map) {
const mapCopy = new Map();
value.forEach((val, key) => {
mapCopy.set(deepClone(key), deepClone(val));
});
return mapCopy;
}
// 处理 Set
if (value instanceof Set) {
const setCopy = new Set();
value.forEach(item => {
setCopy.add(deepClone(item));
});
return setCopy;
}
// 处理普通对象
const newObj = Object.create(Object.getPrototypeOf(value)); // 保留原型
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepClone(value[key]); // 递归处理
}
}
return newObj;
}
deepClone
中我保留了原型,深拷贝后的对象与原对象共用原型,所以如果原型上有变化,都会发生变化- 对于原型的处理,可以根据实际情况自己调整
- 保留原型
- 丢弃原型
- 继续遍历原型,将原型也都复制一遍