1.作用域
作用域链
作用域链本质上是底层的变量查找机制
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
垃圾回收机制
JavaScript主要采用两种垃圾回收算法:标记-清除(Mark-and-Sweep)和引用计数(Reference Counting)。
标记-清除(Mark-and-Sweep)
- 标记阶段:垃圾回收器首先遍历所有可达的对象(即从根对象开始,通过引用关系可以访问到的所有对象),并将它们标记为“活着”。根对象包括全局变量、执行上下文中的变量、函数调用栈中的变量等。
- 清除阶段:遍历完所有可达对象后,垃圾回收器会再次遍历堆内存中的所有对象,未被标记的对象被视为不再使用,其占用的内存将在这一阶段被回收。
引用计数(Reference Counting)
这是一种更简单的垃圾回收策略,每个对象都有一个引用计数,每当有一个新的引用指向该对象时,引用计数加1;当引用被删除或失去作用域时,引用计数减1。当一个对象的引用计数降为0时,表示没有变量再引用它,此时就可以直接释放该对象所占用的内存。
优缺点:
- 标记-清除的优点在于能够处理循环引用的问题,而引用计数机制则无法处理这种情况。但是,标记-清除算法在执行时会暂停其他脚本执行,可能导致应用暂时失去响应(即所谓的“全停顿”)。
- 引用计数简单高效,但存在循环引用的问题,即两个对象相互引用但不再被外部访问时,它们的引用计数不会降为0,导致内存泄漏。
闭包
闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量,可以创建私有变量,避免全局污染。
// 闭包的写法 统计函数的调用次数
function outer() {
let count = 1
function fn() {
count++
console.log(`函数被调用${count}次`)
}
return fn
}
const re = outer()
// const re = function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
re()
- 闭包 = 内层函数 + 外层函数的变量
但是存在内存泄露的问题
2.函数
默认值
<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
</script>
总结:
- 声明函数时为形参赋值即为参数的默认值
- 如果参数未自定义默认值时,参数的默认值为
undefined
- 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
动态参数arguments
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
arguments
的作用是动态获取函数的实参
<script>
// 求和函数,计算所有参数的和
function sum() {
let s = 0
for(let i = 0; i < arguments.length; i++) {
s += arguments[i]
}
console.log(s)
}
// 调用求和函数
sum(5, 10)// 两个参数
sum(1, 2, 4) // 两个参数
</script>
剩余参数
<script>
function config(baseURL, ...other) {
console.log(baseURL) // 得到 'http://baidu.com'
console.log(other) // other 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
</script>
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参- 借助
...
获取的剩余实参,是个真数组
*箭头函数
箭头函数属于表达式函数,因此不存在函数提升
- 基本语法:
(param1, param2, ..., paramN) => { statements }
// 对于只有一个参数的情况,圆括号可以省略:
param => { statements }
// 如果函数体只有一条语句,并且这条语句的返回值就是函数的返回值,那么可以进一步简化,省略花括号:
(param1, param2, ..., paramN) => expression
// 或者
param => expression
- 箭头函数中没有
arguments
,只能使用...
动态获取实参
<body>
<script>
// 1. 利用箭头函数来求和
const getSum = (...arr) => {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
const result = getSum(2, 3, 4)
console.log(result) // 9
</script>
-
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
// 箭头函数的this 是上一层作用域的this 指向 const fn = () => { console.log(this) // window } fn() // 对象方法箭头函数 this const obj = { uname: 'ldh', sayHi: () => { console.log(this) // this 指向谁? window } } obj.sayHi() const obj = { uname: 'ldh', sayHi: function () { console.log(this) // obj let i = 10 const count = () => { console.log(this) // obj } count() } } obj.sayHi()
-
箭头函数不能用作构造函数,也就是说不能使用
new
关键字调用箭头函数。
3.展开运算符...
数组展开
- 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // 结果: [1, 2, 3, 4, 5, 6]
- 克隆数组:
const originalArray = [1, 2, 3];
const clonedArray = [...originalArray]; // 浅克隆
- 将字符串或类数组对象转换为数组:
const str = 'abc';
const arrayFromString = [...str]; // 结果: ['a', 'b', 'c']
const nodeList = document.querySelectorAll('div');
const divs = [...nodeList]; // 将NodeList转换为数组
函数参数传递
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 结果: 6
对象展开
对于对象,展开运算符可以用于复制对象并添加或覆盖属性,或者将多个对象合并成一个新的对象:
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // 复制并添加新属性(浅拷贝)
const mergedObj = { ...obj1, ...obj2, b: 3 }; // 合并对象并覆盖属性
注意事项
- 展开运算符不会展开对象的原型链上的属性。
4.解构
JavaScript中的解构(Destructuring)是一种表达式,它允许你从数组或者对象中提取数据,将其赋值给变量。这种语法极大地简化了数据处理,使得代码更简洁、可读性更强。解构分为两种主要类型:数组解构和对象解构。
数组解构
let arr = [1, 2, 3];
let [a, b, c] = arr;
console.log(a); // 输出 1
console.log(b); // 输出 2
console.log(c); // 输出 3
可以使用逗号 ,
来跳过某些元素。
let arr = [1, 2, 3, 4, 5];
let [first, , third] = arr;
console.log(first); // 输出 1
console.log(third); // 输出 3
尝试解构的值不存在或者为undefined
时,可以设置默认值,且只有单元值为 undefined
时默认值才会生效。
let arr = [1, 2];
let [a, b, c = 3] = arr;
console.log(a); // 输出 1
console.log(b); // 输出 2
console.log(c); // 输出 3
对象解构
对象解构允许你将对象的属性值直接赋给同名的变量。
let obj = {name: 'Alice', age: 30};
let {name, age} = obj;
console.log(name); // 输出 'Alice'
console.log(age); // 输出 30
可以解构嵌套的对象,只需要用点操作符或者方括号表示路径。
let user = {info: {name: 'Bob', age: 25}};
let {info: {name}} = user;
console.log(name); // 输出 'Bob'
可以为解构的属性设置默认值,属性不存在或单元值为 undefined
时默认值才会生效,或者使用不同的变量名来接收属性值(即别名)。
let obj = {name: 'Charlie'};
let {name: userName = 'Default', address = 'Unknown'} = obj;
console.log(userName); // 输出 'Charlie'
console.log(address); // 输出 'Unknown'
5.数组方法
forEach遍历数组
用于遍历数组的每个元素,并对每个元素执行提供的函数(回调函数)。这个方法不会创建新数组,也不会改变原数组,**纯粹是为了执行回调函数遍历数组而存在。**它不返回任何值(返回值是 undefined
)。
<body>
<script>
// forEach 就是遍历 加强版的for循环 适合于遍历数组对象
const arr = ['red', 'green', 'pink']
const result = arr.forEach(function (item, index) {
console.log(item) // 数组元素 red green pink
console.log(index) // 索引号
})
</script>
</body>
filter筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组
<body>
<script>
const arr = [10, 20, 30]
// const newArr = arr.filter(function (item, index) {
// return item >= 20
// })
// 返回的符合条件的新数组
const newArr = arr.filter(item => item >= 20)
console.log(newArr)
</script>
</body>
6.构造函数
构造函数是专门用于创建对象的函数,如果一个函数使用 new
关键字调用,那么这个函数就是构造函数。
<script>
// 定义函数
function foo() {
console.log('通过 new 也能调用函数...');
}
// 调用函数
new foo;
</script>
总结:
-
实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。
-
使用
new
关键字调用函数的行为被称为实例化 -
实例化构造函数时没有参数时可以省略
()
-
构造函数的返回值即为新创建的对象
-
构造函数内部的
return
返回的值无效!
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
<script>
// 构造函数
function Person() {
// 构造函数内部的 this 就是实例对象
// 实例对象中动态添加属性
this.name = '小明'
// 实例对象动态添加方法
this.sayHi = function () {
console.log('大家好~')
}
}
// 实例化,p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person()
console.log(p1)
console.log(p1.name) // 访问实例属性
p1.sayHi() // 调用实例方法
</script>
总结:
- 构造函数内部
this
实际上就是实例对象,为其动态添加的属性和方法即为实例成员 - 为构造函数传入参数,动态创建结构相同但值不同的对象
注:构造函数创建的实例对象彼此独立互不影响。
静态成员
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
<script>
// 构造函数
function Person(name, age) {
// 省略实例成员
}
// 静态属性
Person.eyes = 2
Person.arms = 2
// 静态方法
Person.walk = function () {
console.log('^_^人都会走路...')
// this 指向 Person
console.log(this.eyes)
}
</script>
总结:
- 静态成员指的是添加到构造函数本身的属性和方法
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的
this
指向构造函数本身
原型对象
JavaScript中的每个函数都有一个prototype
属性,这个属性是一个对象,用于继承。当构造函数创建新实例时,这些实例会从构造函数的prototype
继承属性和方法。这意味着所有实例都可以访问定义在原型上的方法,从而节省内存。
- 构造函数和原型对象中的this 都指向 实例化的对象
<script>
function Person() {
// 此处定义同名方法 sayHi
this.sayHi = function () {
console.log('嗨!');
}
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~');
}
let p1 = new Person();
p1.sayHi(); // 输出结果为 嗨!
</script>
当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。
constructor 属性
每个由构造函数创建的对象都自动拥有一个constructor
属性,指向创建它的构造函数。
function Person(name, age) {
this.name = name;
this.age = age;
// 可以在这里定义方法
this.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
}
let person1 = new Person('Alice', 30);
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
let person2 = new Person('Bob', 25);
person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.
console.log(person1.constructor === Person); // 输出: true
Person.prototype.sayGoodbye = function() {
console.log(`Goodbye, my name is ${this.name}.`);
};
person1.sayGoodbye(); // 输出: Goodbye, my name is Alice.
person2.sayGoodbye(); // 输出: Goodbye, my name is Bob.
7.面向对象思想
封装性
js面向对象可以通过构造函数实现的封装。
同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的。
<script>
function Person() {
this.name = '佚名'
// 设置名字
this.setName = function (name) {
this.name = name
}
// 读取名字
this.getName = () => {
console.log(this.name)
}
}
// 实例对像,获得了构造函数中封装的所有逻辑
let p1 = new Person()
p1.setName('小明')
console.log(p1.name)
// 实例对象
let p2 = new Person()
console.log(p2.name)
</script>
继承性
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承
对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
查看原型: 你可以使用__proto__
来查看一个对象的原型。
const obj = {};
console.log(obj.__proto__); // 这将输出对象的原型,即Object.prototype
原型链:在JavaScript中,每个对象都有一个原型对象(prototype object),当试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎会继续在其原型对象中查找(也就是 __proto__指向的 prototype 原型对象),这个过程会一直向上追溯,直到找到该属性或方法,或者达到原型链的末端(通常是
null
)。
<body>
<script>
function Person() {
this.eyes = 2
this.head = 1
}
// console.log(new Person)
// 女人 构造函数 继承 想要 继承 Person
function Woman() {
}
// Woman 通过原型来继承 Person
Woman.prototype = new Person() // {eyes: 2, head: 1}
// 指回原来的构造函数
Woman.prototype.constructor = Woman
// 给女人添加一个方法 生孩子
Woman.prototype.baby = function () {
console.log('宝贝')
}
const red = new Woman()
console.log(red)
// console.log(Woman.prototype)
// 男人 构造函数 继承 想要 继承 Person
function Man() {
}
// 通过 原型继承 Person
Man.prototype = new Person()
Man.prototype.constructor = Man
const pink = new Man()
console.log(pink)
</script>
</body>
instanceof 运算符
可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
console.log(Object.prototype)
console.log(Object.prototype.__proto__) //null
function Person() {
}
const ldh = new Person()
console.log(ldh instanceof Person)
console.log(ldh instanceof Object)
console.log(ldh instanceof Array)
console.log([1, 2, 3] instanceof Array)
console.log(Array instanceof Object)
8.改变this的指向
有 3 个方法可以动态指定普通函数中 this
的指向:
call
使用 call
方法调用函数,同时指定函数中 this
的值,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.call(user); // this 值为 user
sayHi.call(student); // this 值为 student
// 求和函数
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result);
</script>
总结:
call
方法能够在调用函数的同时指定this
的值- 使用
call
方法调用函数时,第1个参数为this
指定的值 call
方法的其余参数会依次自动传入函数做为函数的参数
apply
使用 call
方法调用函数,同时指定函数中 this
的值,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.apply(user) // this 值为 user
sayHi.apply(student) // this 值为 student
// 求和函数
function counter(x, y) {
return x + y
}
// 调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10])
console.log(result)
</script>
总结:
apply
方法能够在调用函数的同时指定this
的值- 使用
apply
方法调用函数时,第1个参数为this
指定的值 apply
方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind
bind
方法并不会调用函数,而是创建一个指定了 this
值的新函数,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
// 调用 bind 指定 this 的值
let sayHello = sayHi.bind(user);
// 调用使用 bind 创建的新函数
sayHello()
</script>