一、作用域
1.1 Js中有全局作用域和局部作用域。
1.全局作用域
全局作用域是指在代码中任何地方都可以访问的变量,而局部作用域是指在特定代码块内部定义的变量,只能在该代码块内部访问。
2.分为函数作用域和块作用域
函数创建了局部作用域,变量在函数内声明时成为局部变量,只能在函数内部访问。函数执行后会被清理,函数间互不影响
块作用域是把{}
的代码块,内部的变量有些声明出的变量是不会在外部被访问。
let
、const
声明的会产生块作用域;
var
是不会其在内部声明外部依然可以引用
3. 作用域链
当代码中访问一个变量时,Js引擎会首先查找当前作用域,如果找不到,就会向上一级作用域继续查找,直到找到该变量或者到达全局作用域为止。作用域链的形成是由函数的嵌套关系所决定的。
4.垃圾回收
垃圾回收是一种自动管理内存的机制,用于检测不再被程序使用的内存,并释放这些内存以便重用。在Js中,垃圾回收器会定期检查不再需要的变量和对象,并释放它们占用的内存空间。这有助于减少内存泄漏和提高程序的性能。Js使用的主要垃圾回收策略是标记-清除算法,它标记所有活动对象,然后清除未被标记的对象。
1.2 闭包
闭包是指函数和函数内部能访问到的其外部作用域的组合。在Js中,函数可以访问其外部作用域中的变量,即使函数在外部作用域执行完毕后仍然可以访问这些变量。这种行为形成了闭包,使函数可以“记住”其创建时所处的作用域。闭包在实际开发中常用于保持变量的状态、实现模块化和封装等。
function outerFunction() {
let outerVar = 'I am from the outer function';
function innerFunction() {
console.log(outerVar); // innerFunction可以访问outerFunction中的outerVar变量
}
return innerFunction;
}
let closureExample = outerFunction();
closureExample(); // 输出: I am from the outer function
1.2.1 闭包的作用包括:
- 保持变量的状态:闭包可以使函数保持对其创建时作用域中变量的引用,从而保持这些变量的状态。
- 实现模块化:通过闭包可以创建私有变量和函数,实现模块化开发,避免全局命名冲突。
- 延长变量的生命周期:闭包可以延长变量的生命周期,使得在函数执行完毕后仍然可以访问这些变量。
1.2.2 优点:
- 可以实现数据封装和隐藏,提高代码的安全性。
- 可以减少全局变量的污染,避免命名冲突。
- 可以实现函数式编程的一些特性,如柯里化和偏函数应用。
1.2.3 缺点:
- 可能导致内存泄漏:如果闭包中持有对大量变量的引用,这些变量可能无法被垃圾回收,导致内存泄漏。
- 可能增加代码复杂性:过度使用闭包可能导致代码难以理解和维护。
- 可能影响性能:闭包会增加内存消耗和函数调用的开销,可能影响性能。
1.3 变量提升
变量提升是Js中的一种特性,指在代码执行过程中,Js引擎会将变量声明提升到其作用域的顶部,但不会提升变量的赋值。这意味着可以在变量声明之前访问这些变量,但在赋值之前会得到undefined
。函数声明也会被提升到作用域的顶部,因此可以在函数声明之前调用这些函数。这种行为可能会导致一些意外的结果,因此建议在变量使用前先进行声明和赋值。
console.log(myVar); // 输出: undefined
var myVar = 'Hello';
// 上述代码实际上会被解释为以下形式:
var myVar;
console.log(myVar); // 输出: undefined
myVar = 'Hello';
// 函数声明也会被提升
sayHello(); // 输出: Hello
function sayHello() {
console.log('Hello');
}
二、函数
2.1 提升
2.1.1 可提升
函数提升是Js中的一种特性,指在代码执行过程中,函数声明会被提升到其作用域的顶部,使得可以在函数声明之前调用这些函数。这意味着可以在函数声明之前调用函数,而不会出现引用错误。函数表达式(如匿名函数赋值给变量)不会被提升。
sayHello(); // 输出: Hello
function sayHello() {
console.log('Hello');
}
在这个例子中,函数sayHello
被提升到作用域的顶部,因此可以在函数声明之前调用sayHello
函数而不会报错。
2.1.2 不可
函数表达式不会被提升是因为函数表达式实际上是将函数赋值给一个变量,而变量的声明和赋值是分开的过程。在Js中,只有变量声明会被提升,而变量赋值部分不会被提升。因此,对于函数表达式,只有变量声明会被提升,而函数赋值部分仍然会按照代码顺序执行。
sayHello(); // 报错: sayHello is not a function
var sayHello = function() {
console.log('Hello');
};
在这个例子中,虽然var sayHello
会被提升,但是函数赋值部分并没有被提升,因此在调用sayHello
时会报错。
2.2 函数参数(动态、剩余)
动态参数和剩余参数都是用于处理函数接受可变数量参数的情况。
-
动态参数(arguments):
arguments
对象是一个类数组对象,包含了函数调用时传入的所有实参。它允许在函数内部访问所有传入的参数,即使这些参数没有在函数定义中声明形参。 -
剩余参数(rest parameters):剩余参数使用三个点(…)表示,允许将不定数量的参数表示为一个数组。剩余参数只能在函数的最后一个参数位置使用,用于捕获函数调用时传入的多余参数。
// 动态参数示例
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3)); // 输出: 6
// 剩余参数示例
function multiply(multiplier, ...nums) {
return nums.map(num => num * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // 输出: [2, 4, 6]
注意事项:
- 动态参数可以在任何函数中使用,但剩余参数只能在函数的最后一个参数位置使用。
- 建议尽量使用剩余参数而不是
arguments
对象,因为剩余参数更易读、更灵活,并且是真正的数组。 - 在使用剩余参数时,注意剩余参数是一个真正的数组,可以使用数组的方法和属性。
2.2.1 展开运算符
展开运算符(Spread Operator)是在ES6中引入的语法,用三个点(…)表示。它可以在函数调用、数组字面量和对象字面量中展开可迭代对象(如数组、字符串、对象等),将其拆分为独立的值。
展开运算符的主要作用包括:
- 在函数调用中传递数组元素作为参数。
- 在数组字面量中创建新数组,合并数组或复制数组。
- 在对象字面量中创建新对象,合并对象或复制对象。
// 在函数调用中使用展开运算符
function sum(x, y, z) {
return x + y + z;
}
let numbers = [1, 2, 3];
console.log(sum(...numbers)); // 输出: 6
// 在数组字面量中使用展开运算符
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let mergedArr = [...arr1, ...arr2];
console.log(mergedArr); // 输出: [1, 2, 3, 4, 5, 6]
// 在对象字面量中使用展开运算符
let obj1 = {
a: 1, b: 2 };
let obj2 = {
c: 3, d: 4 };
let mergedObj = {
...obj1, ...obj2 };
console.log(mergedObj); // 输出: { a: 1, b: 2, c: 3, d: 4 }
注意事项:
- 展开运算符只能用于可迭代对象,如数组、字符串、对象等。
- 在对象字面量中使用展开运算符时,如果有重复的属性名,后面的属性会覆盖前面的属性。
- 在函数调用中使用展开运算符时,展开运算符后面的元素会依次传递给函数的参数。
2.2.2 选择使用展开运算符还是剩余参数取决于具体的使用场景:
剩余:函数参数的使用,得到真数组
展开:数组中使用,数组展开
-
使用展开运算符:
- 当需要将一个数组或对象展开为独立的值时,可以使用展开运算符。
- 在函数调用时,如果已经有一个数组或对象,希望将其拆分为独立的参数传递给函数时,可以使用展开运算符。
- 在创建新数组或对象时,需要合并多个数组或对象的内容时,展开运算符是一个方便的选择。
-
使用剩余参数:
- 当需要处理不定数量的参数,并将它们作为一个数组处理时,应该使用剩余参数。
- 在函数定义时,如果希望函数能够接受任意数量的参数,并将这些参数作为一个数组处理,应该使用剩余参数。
- 剩余参数提供了更灵活的方式来处理函数接受的参数,尤其是在参数数量不确定的情况下。
综合来说,如果需要将一个可迭代对象展开为独立的值,或者在函数调用时将已有的数组或对象拆分为独立的参数,可以使用展开运算符;如果需要处理不定数量的参数,并将它们作为一个数组处理,应该使用剩余参数。
2.3 箭头函数
箭头函数是ES6中引入的一种新的函数定义方式,它提供了一种更简洁的语法形式。箭头函数使用箭头符号(=>)来定义函数,可以减少函数定义时的样板代码,并且具有词法作用域绑定的特性。
主要特点包括:
- 省略了
function
关键字,使得函数定义更为简洁。 - 如果箭头函数只有一个参数,可以省略参数的括号。
- 如果箭头函数只有一条返回语句,可以省略大括号和
return
关键字。
// 普通函数
function multiply(x, y) {
return x * y;
}
// 箭头函数
const multiply = (x, y) => x * y;
// 箭头函数省略参数括号和大括号
const greet = name => `Hello, ${
name}!`;
// 箭头函数与数组的结合
const numbers = [1, 2, 3];
const squaredNumbers = numbers.map(num => num * num);
箭头函数有一些注意事项:
- 箭头函数没有自己的
this
,它会捕获所在上下文的this
值。 - 不能用作构造函数,不能使用
new
关键字调用箭头函数。 - 不适合用于定义对象方法,因为没有自己的
this
,会导致上下文混乱。
总的来说,箭头函数适合用于简单的函数定义,特别是在需要保持上下文一致性和减少样板代码时非常有用。
2.3.1 箭头函数参数
箭头函数的参数可以有多种形式,取决于参数的数量和是否需要使用默认参数值:
- 无参数:
const greet = () => {
return 'Hello!';
};
- 单个参数:
const greet = name => {
return `Hello, ${
name}!`;
};
- 多个参数:
const add = (a, b) => {
return a + b;
};
- 默认参数值:
const greet = (name = 'Guest') => {
return `Hello, ${
name}!`;
};
- 剩余参数:
const sum = (...numbers) => {
return numbers.reduce((acc, num) => acc + num, 0);
};
在箭头函数中,参数的括号是可选的,但在以下情况下需要使用括号:
- 当没有参数或有多个参数时,需要使用括号。
- 当使用默认参数值或剩余参数时,也需要使用括号。
箭头函数的参数形式非常灵活,可以根据具体的需求选择合适的形式来定义函数。
2.3.2 this
this是找调用者、函数调用者,箭头函数与不创建自己的this,会在自己作用域的上一级沿用this
箭头函数与普通函数不同之处之一在于它没有自己的this
绑定,而是捕获(capture)了所在上下文的this
值。这意味着箭头函数内部的this
值与外部的this
值保持一致,不会因为函数被调用的方式而改变。
function Person() {
this.age = 0;
// 普通函数
setInterval(function growUp() {
this.age++;
console.log(this.age); // 输出: NaN
}, 1000);
// 箭头函数
setInterval(() => {
this.age++;
console.log(this.age); // 输出: 1, 2, 3, ...
}, 1000);
}
const person = new Person();
在上面的例子中,普通函数中的this
会指向window
对象(在浏览器环境下),导致this.age++
操作实际上是对window.age
进行操作,最终输出NaN
。而箭头函数中的this
捕获了Person
对象的this
,因此能够正确地增加age
属性的值。
因此,箭头函数适合在需要保持上下文一致性的情况下使用,特别是在回调函数或嵌套函数中,可以避免this
指向发生变化的问题。
三、解构赋值
Js中的解构赋值可以让你从数组或对象中提取数据并赋值给变量
3.1 数组解构
数组解构赋值允许你从数组中提取值并将它们赋给变量,顺序与数组中的顺序相对应。
let person = ['Alice', 30];
// 使用解构赋值提取数组中的值
let [name, age] = person;
console.log(name); // Alice
console.log(age); // 30
在这个例子中,name
变量将被赋值为数组中的第一个元素,age
变量将被赋值为数组中的第二个元素。
3.1.1 交换变量
数组开头一定要加上分号
let a = 1
let b = 2
;[b,a] = [a,b]
3.1.2 立即执行函数
同样的需加上分号
立即执行函数是在定义后立即执行的函数。它有两种常见的写法:
- 使用括号包裹函数表达式:
(function() {
console.log('立即执行函数');
})();
- 使用括号包裹整个函数:
(function() {
console.log('立即执行函数');
}());
这种模式通常用于创建作用域,避免变量污染全局作用域。
3.1.3 注意事项
(1)当解构赋值中的变量数量与数组中的元素数量不一致时,未被对应的变量将会被赋值为undefined
。
let numbers = [1, 2, 3];
// 只提取前两个元素
let [a, b] = numbers;
console.log(a); // 1
console.log(b); // 2
// 第三个元素没有对应的变量,将被忽略
(2)如果是数组结构,可以使用剩余参数(rest parameter)来捕获剩余的元素。
let numbers = [1, 2, 3, 4, 5];
// 使用剩余参数捕获剩余的元素
let [a, b, ...rest] = numbers;
console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]
在这个例子中,a
和b
分别获取了数组中的前两个元素,而rest
则捕获了剩余的元素作为数组。
(3)
你可以为解构赋值中的变量设置默认值,以防提取的值为undefined
。
let numbers = [1];
// 设置默认值
let [a, b = 2] = numbers;
console.log(a); // 1
console.log(b); // 2
在这个例子中,由于numbers
数组只有一个元素,变量b
会使用默认值2。
(4)如果你想忽略某些返回值,可以使用逗号来跳过特定位置的元素。
let numbers = [1, 2, 3, 4, 5];
// 忽略第二个元素
let [a, , c] = numbers;
console.log(a); // 1
console.log(c); // 3
在这个例子中,我们使用逗号来跳过第二个元素,只提取了第一个和第三个元素。
3.2 对象解构
// 对象解构示例
const person = {
name: 'Alice',
age: 30,
city: 'New York'
};
// 从对象中提取属性
const {
name, age } = person;
console.log(name); // 输出: Alice
console.log(age); // 输出: 30
// 从对象中提取属性并赋值给新变量
const {
name: personName, age: personAge } = person;
console.log(personName); // 输出: Alice
console.log(personAge); // 输出: 30
多级
// 多级对象解构示例
const person = {
name: 'Alice',
age: 30,
address: {
city: 'New York',
country: 'USA'
}
};
// 从多级对象中提取属性
const {
name, address: {
city, country } } = person;
console.log(name); // 输出: Alice
console.log(city); // 输出: New York
console.log(country); // 输出: USA
3.3 遍历数组的方法
如果您想在forEach
方法中访问元素本身以及其索引,您可以传递一个回调函数,该函数接受三个参数:元素本身、索引和数组本身。
const array = ['a', 'b', 'c'];
array.forEach((element, index) => {
console.log(`Element at index ${
index} is ${
element}`);
});
在这个例子中,element
代表数组中的元素,index
代表元素的索引。
filter
方法用于过滤数组中的元素,并返回符合条件的元素组成的新数组。
const numbers = [1, 2, 3, 4, 5];
// 过滤出所有大于2的元素
const filteredNumbers = numbers.filter(num => num > 2);
console.log(filteredNumbers); // 输出: [3, 4, 5]
四、对象
4.1 创建对象:
- 使用对象字面量:
const obj = {
key: value };
可以追加属性
var obj = new Object();
obj.name = '小明';
console.log(obj);
- 使用构造函数:
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person('Alice', 30);
- 使用Object.create:
const obj = Object.create(null);
obj.key = value;
- 使用类(ES6中引入的):
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person1 = new Person('Bob', 25);
4.2 构造函数
构造函数是用来创建对象的函数。通常与new
关键字一起使用来实例化对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person('Alice', 30);
Person
是一个构造函数,它接受name
和age
作为参数,并将它们分配给新创建的对象的属性。通过使用new
关键字,我们可以实例化一个新的Person
对象并将其赋给person1
变量。
4.3 实例成员 & 静态成员
实例成员是每个对象实例都会拥有的成员,它们存在于对象的实例中。可以通过在构造函数中使用this
关键字来定义实例成员。
静态成员是属于构造函数本身的成员,而不是属于实例的。可以通过在构造函数本身上直接定义属性或方法来创建静态成员。
function MyClass() {
// 实例成员
this.instanceProperty = 'Instance Property';
}
// 添加实例方法
MyClass.prototype.instanceMethod = function() {
console.log('Instance Method');
};
// 静态成员
MyClass.staticProperty = 'Static Property';
// 添加静态方法
MyClass.staticMethod = function() {
console.log('Static Method');
};
// 创建实例
const obj = new MyClass();
// 访问实例成员
console.log(obj.instanceProperty); // 输出: Instance Property
obj.instanceMethod(); // 输出: Instance Method
// 访问静态成员
console.log(MyClass.staticProperty); // 输出: Static Property
MyClass.staticMethod(); // 输出: Static Method
4.4 常见的
4.4.1 Object
创建对象:
- 使用
new Object()
可以创建一个空对象。 - 使用
new Object({ key: 'value' })
可以创建一个包含属性的对象。 - 使用对象字面量
{ key: 'value' }
是更常见的创建对象的方式。
常用静态方法:
-
Object.keys(obj)
:返回一个包含给定对象的所有可枚举属性的键的数组。 -
Object.values(obj)
:返回一个包含给定对象的所有可枚举属性的值的数组。 -
Object.entries(obj)
:返回一个包含给定对象的所有可枚举属性的键值对数组。 -
Object.assign(target, ...sources)
:将一个或多个源对象的所有可枚举属性复制到目标对象,并返回目标对象。 -
Object.freeze(obj)
:冻结一个对象,防止对其进行修改。 -
Object.seal(obj)
:封闭一个对象,防止添加或删除属性,但允许修改现有属性。 -
Object.create(proto, propertiesObject)
:使用指定的原型对象和属性创建一个新对象。
4.4.2 Array
-
forEach(callback)
:- 作用:对数组中的每个元素执行提供的回调函数。
- 能力:遍历数组中的每个元素,但不返回新数组。
const arr = [1, 2, 3]; arr.forEach(item => console.log(item));
-
filter(callback)
:- 作用:创建一个新数组,其中包含通过提供的函数实现的测试的所有元素。
- 能力:过滤数组中的元素,返回满足条件的新数组。
const arr = [1, 2, 3, 4]; const filteredArr = arr.filter(item => item > 2);
-
map(callback)
:- 作用:创建一个新数组,其结果是对原数组中的每个元素调用一个提供的函数。
- 能力:对数组中的每个元素执行回调函数,并返回新数组。
const arr = [1, 2, 3]; const mappedArr = arr.map(item => item * 2);
-
reduce(callback, initialValue)
:- 作用:对数组中的每个元素执行一个提供的函数,将其结果汇总为单个值。
- 能力:将数组中的元素累积为一个值,可以是数字、对象等。
const arr = [1, 2, 3, 4]; const sum = arr.reduce((acc, curr) => acc + curr, 0);
这些方法是在Js中处理数组时非常有用的工具,可以帮助你遍历、过滤、映射和汇总数组中的元素。
方法
除了forEach
、filter
、map
和reduce
之外,Array
对象还提供了许多其他常见的方法
-
find(callback)
:- 作用:返回数组中满足提供的测试函数的第一个元素的值。
-
some(callback)
:- 作用:检查数组中是否至少有一个元素满足提供的测试函数。
-
every(callback)
:- 作用:检查数组中的所有元素是否满足提供的测试函数。
-
sort(compareFunction)
:- 作用:对数组元素进行排序。
-
includes(value)
:- 作用:判断数组是否包含特定值。
4.4.3 String
-
属性
length
:-
作用:用于获取字符串的长度。
const str = "Hello, world!"; console.log(str.length); // 输出: 13
-
-
方法
split(separator)
:-
作用:用来将字符串分割成数组。
const str = "Hello, world!"; const arr = str.split(", "); console.log(arr); // 输出: ["Hello", "world!"]
-
-
方法
substring(startIndex[, endIndex])
:- 作用:用于字符串截取。
const str = "Hello, world!"; const subStr = str.substring(0, 5); console.log(subStr); // 输出: "Hello"
- 作用:用于字符串截取。
-
方法
startsWith(searchString[, position])
:- 作用:检测字符串是否以某字符开头。
const str = "Hello, world!"; console.log(str.startsWith("Hello")); // 输出: true
- 作用:检测字符串是否以某字符开头。
-
方法
includes(searchString[, position])
:- 作用:判断一个字符串是否包含在另一个字符串中,根据情况返回
true
或false
。const str = "Hello, world!"; console.log(str.includes("world")); // 输出: true
- 作用:判断一个字符串是否包含在另一个字符串中,根据情况返回
-
方法
toUpperCase()
:- 作用:用于将字母转换成大写。
const str = "Hello, world!"; console.log(str.toUpperCase()); // 输出: "HELLO, WORLD!"
- 作用:用于将字母转换成大写。
-
方法
toLowerCase()
:- 作用:用于将字母转换成小写。
const str = "Hello, world!"; console.log(str.toLowerCase()); // 输出: "hello, world!"
- 作用:用于将字母转换成小写。
-
方法
indexOf(searchValue[, fromIndex])
:- 作用:检测是否包含某字符,返回字符首次出现的位置。
const str = "Hello, world!"; console.log(str.indexOf("world")); // 输出: 7
- 作用:检测是否包含某字符,返回字符首次出现的位置。
-
方法
endsWith(searchString[, length])
:- 作用:检测是否以某字符结尾。
const str = "Hello, world!"; console.log(str.endsWith("world!")); // 输出: true
- 作用:检测是否以某字符结尾。
-
方法
replace(searchValue, newValue)
:- 作用:用于替换字符串,支持正则匹配。
const str = "Hello, world!"; const newStr = str.replace("world", "Js"); console.log(newStr); // 输出: "Hello, Js!"
- 作用:用于替换字符串,支持正则匹配。
-
方法
match(regexp)
:- 作用:用于查找字符串,支持正则匹配。
const str = "Hello, world!"; const matches = str.match(/world/); console.log(matches); // 输出: ["world"]
- 作用:用于查找字符串,支持正则匹配。
五、原型
在JavaScript中,原型(Prototype)是一个对象,它为其他对象提供共享属性和方法。每个JavaScript对象(除了null)在创建时都会与另一个对象关联,这个对象就是原型。通过原型,JavaScript实现了继承和属性共享。
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 在构造函数的原型上添加方法
Person.prototype.greet = function() {
console.log('Hello, ' + this.name);
};
// 创建一个实例
let person1 = new Person('Alice');
// 调用原型上的方法
person1.greet(); // 输出: Hello, Alice
// 验证原型链
console.log(person1.__proto__ === Person.prototype); // 输出: true
console.log(Person.prototype.__proto__ === Object.prototype); // 输出: true
console.log(Object.prototype.__proto__ === null); // 输出: true
构造函数和原型对象中的this 都指向 实例化的对象
constructor
在JavaScript中,每个对象都有一个constructor
属性,这个属性指向创建该对象的构造函数。constructor
属性通常用于识别对象的类型或重新创建对象。
示例代码
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 创建一个实例
let person1 = new Person('Alice');
// 检查 constructor 属性
console.log(person1.constructor === Person); // 输出: true
// 使用 constructor 属性创建新实例
let person2 = new person1.constructor('Bob');
console.log(person2.name); // 输出: Bob
解释
-
构造函数:
Person
是一个构造函数,用于创建Person
类型的对象。
-
实例对象:
person1
是通过new Person('Alice')
创建的实例对象。
-
constructor 属性:
person1.constructor
指向Person
构造函数。- 通过
person1.constructor
可以创建新的Person
实例。
修改 constructor 属性
有时在修改原型时,可能会意外地改变constructor
属性。可以手动修复它:
function Person(name) {
this.name = name;
}
// 修改原型
Person.prototype = {
greet: function() {
console.log('Hello, ' + this.name);
}
};
// 修复 constructor 属性
Person.prototype.constructor = Person;
let person1 = new Person('Alice');
console.log(person1.constructor === Person); // 输出: true
解释
-
修改原型:
- 直接赋值给
Person.prototype
会覆盖默认的constructor
属性。
- 直接赋值给
-
修复 constructor 属性:
- 手动将
Person.prototype.constructor
重新指向Person
构造函数。
- 手动将
通过理解和正确使用constructor
属性,可以更好地管理对象的类型和实例化过程。
对象原型
在JavaScript中,对象原型(Prototype)是一个对象,它为其他对象提供共享属性和方法。每个JavaScript对象(除了null
)在创建时都会与另一个对象关联,这个对象就是原型。通过原型,JavaScript实现了继承和属性共享。
主要概念
-
原型链(Prototype Chain):
- 每个对象都有一个
__proto__
属性,指向其原型对象。 - 原型对象本身也有一个
__proto__
属性,指向它的原型,形成一个链条,直到Object.prototype
,它的__proto__
为null
。
- 每个对象都有一个
-
构造函数(Constructor Function):
- 使用构造函数创建的对象,其原型会指向构造函数的
prototype
属性。 - 例如,
function Person() {}
,通过new Person()
创建的对象,其原型是Person.prototype
。
- 使用构造函数创建的对象,其原型会指向构造函数的
-
继承(Inheritance):
- 通过原型链,子对象可以继承父对象的属性和方法。
- 例如,
let obj = new Person()
,obj
可以访问Person.prototype
上的属性和方法。
示例代码
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 在构造函数的原型上添加方法
Person.prototype.greet = function() {
console.log('Hello, ' + this.name);
};
// 创建一个实例
let person1 = new Person('Alice');
// 调用原型上的方法
person1.greet(); // 输出: Hello, Alice
// 验证原型链
console.log(person1.__proto__ === Person.prototype); // 输出: true
console.log(Person.prototype.__proto__ === Object.prototype); // 输出: true
console.log(Object.prototype.__proto__ === null); // 输出: true
解释
-
构造函数调用:
- 当使用
new
关键字调用构造函数时,this
指向新创建的对象实例。 - 例如,
let person1 = new Person('Alice');
,此时this
指向person1
。
- 当使用
-
原型方法调用:
- 当调用原型上的方法时,
this
指向调用该方法的对象实例。 - 例如,
person1.greet();
,此时this
指向person1
,所以this.name
是'Alice'
。
- 当调用原型上的方法时,
注意事项
- 如果方法没有通过对象调用,
this
的指向可能会变得不明确,甚至指向全局对象(在严格模式下为undefined
)。 - 可以使用
bind
、call
或apply
方法显式地设置this
的指向。
示例代码(使用bind)
function greet() {
console.log('Hello, ' + this.name);
}
let person1 = {
name: 'Alice' };
let person2 = {
name: 'Bob' };
let greetPerson1 = greet.bind(person1);
let greetPerson2 = greet.bind(person2);
greetPerson1(); // 输出: Hello, Alice
greetPerson2(); // 输出: Hello, Bob
通过以上示例,可以看到this
在不同调用上下文中的指向变化。理解this
的指向对于正确使用原型方法非常重要。
六、拷贝
在JavaScript中,深拷贝和浅拷贝是两种不同的复制对象的方法。
浅拷贝
浅拷贝只复制对象的第一层属性,如果属性是引用类型(如对象、数组),则只复制引用,不复制实际的值。
示例
let obj1 = {
a: 1, b: {
c: 2 } };
let obj2 = Object.assign({
}, obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3,因为 obj1 和 obj2 共享同一个 b 对象
拷贝数组:Array.prototype.concat() 或者 […arr]
深拷贝
深拷贝会递归地复制对象的所有层级,确保每个层级都是独立的副本。
示例
let obj1 = {
a: 1, b: {
c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2,因为 obj1 和 obj2 的 b 对象是独立的
深拷贝的另一种方法(使用递归)
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
let arrCopy = [];
obj.forEach((item, index) => {
arrCopy[index] = deepClone(item);
});
return arrCopy;
}
let objCopy = {
};
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key]);
});
return objCopy;
}
let obj1 = {
a: 1, b: {
c: 2 } };
let obj2 = deepClone(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2,因为 obj1 和 obj2 的 b 对象是独立的
总结
- 浅拷贝:只复制对象的第一层属性,引用类型的属性仍然共享同一个引用。
- 深拷贝:递归地复制对象的所有层级,确保每个层级都是独立的副本。
直接赋值和浅拷贝有显著的区别
主要体现在它们如何处理对象和数组等引用类型的数据。
直接赋值
直接赋值只是复制对象或数组的引用,而不是创建一个新的副本。因此,修改新变量会影响原始变量。
示例
let obj1 = {
a: 1, b: {
c: 2 } };
let obj2 = obj1; // 直接赋值
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3,因为 obj1 和 obj2 共享同一个 b 对象
浅拷贝
浅拷贝会创建一个新对象或数组,并复制原始对象或数组的第一层属性。如果属性是引用类型(如对象、数组),则只复制引用,不复制实际的值。
示例
let obj1 = {
a: 1, b: {
c: 2 } };
let obj2 = Object.assign({
}, obj1); // 浅拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3,因为 obj1 和 obj2 共享同一个 b 对象
区别总结
- 直接赋值:新变量和原始变量共享同一个引用,修改一个会影响另一个。
- 浅拷贝:创建一个新对象或数组,复制第一层属性,但引用类型的属性仍然共享同一个引用。
代码对比
// 直接赋值
let obj1 = {
a: 1, b: {
c: 2 } };
let obj2 = obj1; // 直接赋值
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3
// 浅拷贝
let obj3 = {
a: 1, b: {
c: 2 } };
let obj4 = Object.assign({
}, obj3); // 浅拷贝
obj4.b.c = 3;
console.log(obj3.b.c); // 输出 3
通过以上示例可以看出,直接赋值和浅拷贝在处理引用类型数据时的行为是相同的,都会共享引用类型的属性。
七、异常处理
throw
:throw
关键字用于抛出异常。当出现错误时,可以使用throw
语句来抛出一个自定义的异常。try/catch
:try/catch
语句用于捕获异常。代码块放在try
中,如果有异常抛出,会被catch
捕获,从而执行异常处理代码。debugger
:debugger
语句用于在代码中设置断点,以便在调试过程中暂停代码执行,方便开发人员检查代码状态。
例子:
// 使用 throw 抛出异常
function divide(a, b) {
if (b === 0) {
throw "除数不能为0";
}
return a / b;
}
// 使用 try/catch 捕获异常
try {
let result = divide(10, 0);
console.log(result);
} catch (error) {
console.log("捕获到异常:" + error);
}
// 使用 debugger 设置断点
function multiply(a, b) {
debugger;
return a * b;
}
let result = multiply(5, 3);
console.log(result);
八、This
在JavaScript中,普通函数的调用方式决定了this
的值,即"谁调用this的值指向谁"的原则。具体情况如下:
- 普通函数没有明确调用者时,
this
值为window
;在严格模式下,没有调用者时,this
的值为undefined
。 - 箭头函数中的
this
与普通函数完全不同,不受调用方式的影响,实际上箭头函数中并不存在this
。箭头函数会默认绑定外层this
的值,因此在箭头函数中,this
的值和外层的this
是一样的,引用的是最近作用域中的this
。
在开发中需要注意以下情况:
- 使用箭头函数时需要考虑函数中
this
的值,例如在DOM事件回调函数中,使用箭头函数this
为全局的window
,因此不推荐在需要DOM对象的this
的地方使用箭头函数。 - 基于原型的面向对象也不推荐采用箭头函数。
总结:
- 函数内不存在
this
时,沿用上一级的this
。 - 不适用于构造函数、原型函数、DOM事件函数等。
- 适用于需要使用上层
this
的地方。
改变this
的指向可以使用call()
、apply()
或bind()
方法:
call()
:调用函数并指定this
的值。apply()
:调用函数并指定this
的值,参数以数组形式传递。bind()
:不会调用函数,但能改变函数内部this
指向。
这些方法的区别:
call
和apply
会调用函数并改变this
指向,apply
传递参数必须是数组形式。bind
不会调用函数,只改变this
指向。
主要应用场景:
call
用于调用函数并传递参数。apply
经常与数组有关,比如使用数学对象实现数组最大值最小值。bind
用于改变this
指向而不调用函数,例如改变定时器内部的this
指向。
九、优化
在JavaScript中,防抖(Debouncing)和节流(Throttling)是常见的性能优化技术:
- 防抖(Debouncing):
- 防抖是一种技术,用于限制函数在短时间内频繁触发,只有当一定时间间隔内没有新的调用时,才会执行该函数。
- 典型应用场景包括输入框搜索建议、窗口大小调整事件等需要等待一段时间后再执行的操作。
- 实现方式是设置一个定时器,在每次触发事件时先清除之前的定时器,然后重新设置新的定时器。
<!-- HTML部分 -->
<div id="box">鼠标滑过盒子</div>
<script>
const box = document.getElementById('box');
let count = 0;
function updateCount() {
count++;
box.textContent = `数字变化:${
count}`;
}
const debouncedUpdateCount = debounce(updateCount, 500);
box.addEventListener('mouseover', function() {
box.textContent = '鼠标滑过盒子';
box.addEventListener('mousemove', debouncedUpdateCount);
});
box.addEventListener('mouseout', function() {
box.removeEventListener('mousemove', debouncedUpdateCount);
});
function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
</script>
- 节流(Throttling):
- 节流是一种技术,用于限制函数在一定时间内最多执行一次,即控制函数的执行频率。
- 节流可以确保在一定时间间隔内稳定地执行函数,而不会因为频繁触发而导致性能问题。
- 典型应用场景包括滚动事件、鼠标移动事件等需要控制触发频率的操作。
- 实现方式是设置一个标记变量,在函数执行时检查标记变量是否允许执行,如果允许则执行函数并设置标记变量,否则忽略该次触发。
这两种技术在处理频繁触发的事件时非常有用,可以有效减少不必要的函数执行次数,提升页面性能和用户体验。
简单示例:
防抖(Debouncing)示例:
function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
// 调用 debounce 函数创建一个防抖函数
const debouncedFn = debounce(() => {
console.log('Debounced function executed');
}, 300);
// 在事件处理程序中使用防抖函数
window.addEventListener('resize', debouncedFn);
在上面的示例中,debounce
函数接受一个函数和延迟时间作为参数,返回一个新的函数,该函数在延迟时间内只会执行一次。
节流(Throttling)示例:
function throttle(func, delay) {
let canRun = true;
return function() {
if (!canRun) return;
canRun = false;
setTimeout(() => {
func.apply(this, arguments);
canRun = true;
}, delay);
};
}
// 调用 throttle 函数创建一个节流函数
const throttledFn = throttle(() => {
console.log('Throttled function executed');
}, 500);
// 在事件处理程序中使用节流函数
window.addEventListener('scroll', throttledFn);
在上面的示例中,throttle
函数接受一个函数和时间间隔作为参数,返回一个新的函数,该函数在时间间隔内最多执行一次。
库
以下是使用 Lodash 库来实现节流(throttle)和防抖(debounce)的示例代码:
使用 Lodash 实现节流(Throttle):
// 导入 Lodash 库
const _ = require('lodash');
// 创建一个节流函数,限制每 300 毫秒触发一次
const throttledFn = _.throttle(() => {
console.log('Throttled function executed');
}, 300);
// 在事件处理程序中使用节流函数
window.addEventListener('scroll', throttledFn);
使用 Lodash 实现防抖(Debounce):
// 导入 Lodash 库
const _ = require('lodash');
// 创建一个防抖函数,延迟 500 毫秒执行
const debouncedFn = _.debounce(() => {
console.log('Debounced function executed');
}, 500);
// 在事件处理程序中使用防抖函数
window.addEventListener('resize', debouncedFn);
通过使用 Lodash 库中的 throttle
和 debounce
方法,可以方便地实现节流和防抖功能,而无需手动编写节流和防抖函数。