该笔记适合用于复习JS相关内容,以及有一定基础后快速学习JS
内容主要是对知识点的总结,不适合零基础学习
笔记中包含大量的样例代码
笔记主要内容来自于 JavaScript核心基础_讲师(李立超)_JS教程
1.零散知识
编写位置
<!--1.可以将js编写到网页内部的script标签-->
<script>
alert("哈哈!")
</script>
<!-- 2.可以将js编写外部的js文件中,然后通过script标签进行引入,里面不可再写js代码-->
<script src="./script/script.js"></script>
<!--3.可以将js代码编写到指定属性中-->
<button onclick="alert('你点我干嘛!')">点我一下</button>
<a href="javascript:alert('你被骗了');">超链接</a>
debug
使用debugger
在代码中打断点
严格模式
-
正常模式
- 默认情况下代码都运行在正常模式中,在正常模式,语法检查并不严格,它的原则是:能不报错的地方尽量不报错
- 这种处理方式导致代码的运行性能较差
-
严格模式
- 在严格模式下,语法检查变得严格
- 禁止一些语法
- 更容易报错
- 提升了性能
- 在严格模式下,语法检查变得严格
-
在开发中,应该尽量使用严格模式,这样可以将一些隐藏的问题消灭在萌芽阶段,同时也能提升代码的运行性能
<script>
"use strict" // 全局的严格模式
function fn(){
"use strict" // 函数的严格的模式
}
</script>
注意事项
- JS中每条语句都应该以分号结尾,JS中具有自动添加分号的机制,所以如果不写分号解释器会自动添加。部分情况无法解析,必须要手动添加分号
- 标识符只能含有字母、数字、下划线、$,且不能以数字开头,标识符不能是JS中的关键字和保留字,也不建议使用内置的函数或类名作为变量名
- 变量中并不存储任何值,而是存储值的内存地址
2.数据类型
数值 Number
- 在JS中所有的整数和浮点数都是Number类型
- JS中的数值并不是无限大的,当数值超过一定范围后会显示近似值,所以在JS中进行一些精度比较高的运算时要十分注意
Infinity
是特殊的数值,表示无穷;NaN
也是特殊的数值,表示非法的数值
let a = 10
a = 9.98
a = 999 ** 999 // Infinity
a = Infinity
a = 1 - "a" //NaN
a = NaN
大整数 BigInt
- 大整数用来表示一些比较大的整数
- 大整数使用
n
结尾,它可以表示的数字范围是无限大
let bigN = 999n ** 999n
console.log(bigN)
其他进制数字
- 二进制
0b
- 八进制
0o
- 十六进制
0x
let a
a = 0b1010 // 10
a = 0o10 // 8
a = 0xa4 // 164
类型检查
-
typeof
用来检查不同的值的类型 -
它会根据不同的值返回不同的结果
let a = 10
let b = 10n
console.log(typeof a) // number
console.log(typeof b) // bigint
字符串 String
- 在JS中使用单引号或双引号来表示字符串
- 转义字符,与C语言中的差不多
- 模板字符串,使用反单引号` 来表示模板字符串,可以跨行使用,模板字符串中可以嵌入变量
let name = "Jack"
let str = `他说:“${name},你好!”`
let str2 = "\"You're a fraud!\" He shounted."
布尔值 Boolean
空值 Null
- 空值表示空对象
- 使用
typeof
检查一个空值时会返回"object"(历史遗留bug,实际上无意义),因此使用typeof
无法检查空值
未定义 Undefined
-
当声明一个变量而没有赋值时,它的值就是
Undefined
-
Undefined
类型的值只有一个就是undefined
符号 Symbol
- 用来创建一个唯一的标识
let c = Symbol() // 调用Symbol()创建一个符号
console.log(typeof c) // symbol
总结
JS中原始值一共有七种:Number, BigInt, String, Boolean, Null, Undefined, Symbol
。七种原始值是构成各种数据的基石,原始值在JS中是不可变类型,一旦创建就不能修改。
3.类型转换
转换为字符串
- 调用
toString()
方法将其他类型转换为字符串var.toString()
- 调用
String()
函数将其他类型转换为字符串String(var)
对于拥有toString()
方法的值调用String()
函数时,实际上就是在调用toString()
方法。对于null
,则直接转换为"null",对于undefined
,直接转换为"undefined"。
let a // undefined
a = 10 // 10
a = 666n // 666
a.toString()
console.log(a)
转换为数值
- 使用Number()函数来将其他类型转换为数值
Number(var)
- 字符串:
- 如果字符串是一个合法的数字,则会自动转换为对应的数字
- 如果字符串不是合法数字,则转换为NaN
- 如果字符串是空串或纯空格的字符串,则转换为0
- 布尔值:
- true转换为1,false转换为0
- null 转换为 0
- undefined 转换为 NaN
- 专门用来将字符串转换为数值的两个方法
-
parseInt()
将一个字符串转换为一个整数parseInt(var)
-
解析时,会自左向右读取一个字符串,直到读取到字符串中所有的有效的整数
-
也可以使用
parseInt()
来对一个数字进行取整
-
-
parseFloat()
将一个字符串转换为一个浮点数parseFloat(var)
- 解析时,会自左向右读取一个字符串,直到读取到字符串中所有的有效的小数
let a = '123' // 123
a = 'abc' // NaN
a = '3.14' // 3.14
a = parseInt(a) // 3
a = ' ' // 0
a = null // 0
a = undeinfed // NaN
console.log(Number(a))
let b = '123px' //123
b = 'xp321' //NaN
b = '123.456' // 123
console.log(parseInt(b))
let c = '192.168.0.1' // 192.168
c = '123px' // 123
console.log(parseFloat(c))
转换为布尔值
1.使用Boolean()
函数来将其他类型转换为布尔值
- 数字:
0
和NaN
转换为false
- 其余是
true
- 字符串:
- 空串转换为
false
- 其余是
true
- 空串转换为
null
和undefined
都转换为false
- 对象会转换为
true
- 所有表示空性的没有的错误的值都会转换为
false
:0
、NaN
、空串、null
、undefined
、false
总结
转换为字符串
-
显式转换
String(var)
,var.toString()
-
隐式转换
var + ""
转换为数值
-
显式转换
Number()
-
隐式转换
+var
转换为布尔值
-
显式转换
Boolean()
-
隐式转换
!!var
4.运算符
算数运算符
加减乘除模幂 + - * / % **
JS是一门弱类型语言,当进行运算时会通过自动的类型转换来完成运算
- 当任意一个值和字符串做加法运算时,它会先将其他值转换为字符串,然后再做拼串的操作
- 可以利用这一特点来完成类型转换,可以通过
任意类型 + 一个空串
的形式来将其转换为字符串,其原理和String()函数相同,但使用起来更加简洁。
let a = 10 - '5' // 10 - 5
a = 9 + true // 9 + 1
a = 5 + null // 5 + 0
a = 6 - undeifned // 6 - NaN
let b = '1' + 2 // '1' + '2'
b = 314 + '' // "314"
赋值运算符
等,加减乘除模幂等 = += -= *= /= %= **=
空赋值 ??=
,只有当变量的值为null
或undefined
时才会对变量进行赋值
一元运算符
正负 +-
加加减减 ++ --
,和C语言一致
当我们对非数值类型进行正负运算时,会先将其转换为数值然后再运算
let a = -233
let b = '123'
b = +b // b = Number(b)
逻辑运算符
与或非 && || !
-
如果对一个非布尔值进行取反,它会先将其转换为布尔值然后再取反
-
与或是短路运算符,对于非布尔值进行与运算,它会转换为布尔值然后运算
-
与运算的返回值:
- 如果第一个值为
false
,则直接返回第一个值 - 如果第一个值为
true
,则返回第二个值
- 如果第一个值为
-
或运算的返回值:
- 如果第一个值为
true
,则直接返回第一个值 - 如果第一个值为
false
,则返回第二个值
- 如果第一个值为
let result
//true && true -> true
result = 1 && 2 // 2
//true && false -> false
result = 1 && 0 // 0
//false && true -> false
result = 0 && 2 // 0
//false && false -> false
result = 0 && NaN // 0
result = 1 || 2 // 1
result = "hello" || NaN // "hello"
result = NaN || 1 // 1
result = NaN || null // null
关系运算符
大小于,大小等于 > < >= <=
- 当对非数值进行关系运算时,它会先将前转换为数值然后再比较
- 当关系运算符的两端是两个字符串,它不会将字符串转换为数值,而是逐位的比较字符的Unicode编码,利用这个特点可以对字符串按照字典序排序
- 注意比较两个字符串格式的数字时一定要进行类型转换
let result
result = 5 < "10" // true
result = "1" > false // true
result = "a" < "b" // true
result = "z" < "f" // false
result = "abc" < "b" // true
result = "12" < "2" // true
result = +"12" < "2" // false
相等运算符
==
-
相等运算符,用来比较两个值是否相等
-
使用相等运算符比较两个不同类型的值时,它会将其转换为相同的类型(通常转换为数值)然后再比较,类型转换后值相同也会返回
true
-
null
和undefined
进行相等比较时会返回true
-
NaN
不和任何值相等,包括它自身
-
===
-
全等运算符,用来比较两个值是否全等
-
它不会进行自动的类型转换,如果两个值的类型不同直接返回
false
-
null
和undefined
进行全等比较时会返回false
!=
-
不等,用来检查两个值是否不相等
-
会自动的进行类型转换
!==
-
不全等,用来检查两个值是否不全等
-
不会自动的进行类型转换
let result = 1 == '1' // true
result = true == "1" // true
result = NaN == NaN // false
result = 1 === "1" // false
result = null === undefined // false
条件运算符
condition ? expression1 : expression2
,同C语言
5.流程控制
代码块
使用 {}
来创建代码块,代码块可以用来对代码进行分组
let 和 var
- 在JS中,使用let声明的变量具有块作用域,在代码块中声明的变量无法在代码块的外部访问
- 使用var声明的变量,不具有块作用域
- let必须先声明再使用,否则报Uncaught ReferenceError xxx is not defined。var可以在声明前访问,只是会报undefined
- let变量不能重复声明,var可以
{
let a = 8; // a变量只在花括号内有效
var b = 9;
}
// console.log(a); // ReferenceError: a is not defined
console.log(b); // 9
function func(){
var x = 6
console.log(x)
}
// console.log(x) // Uncaught ReferenceError: x is not defined
流程控制
if-else switch for while do-while break continue
同或类似C语言
提升
变量的提升
- 使用
var
声明的变量,它会在所有代码执行前被声明,所以我们可以在变量声明前就访问变量
函数的提升
- 使用函数声明创建的函数,会在其他代码执行前被创建,所以我们可以在函数声明前调用函数
// console.log(b) // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 10
fn() // 可以执行成功
function fn(){
alert("我是fn函数~")
}
// fn2() //不能执行成功
var fn2 = function(){
alert("我是fn2函数~")
}
console.log(a) // Undefined
var a = 10
6.对象
对象和属性
属性名
-
通常属性名就是一个字符串,所以属性名可以是任何值,没有什么特殊要求
-
但是如果属性名太特殊了,不能直接使用,需要使用[]来设置,虽然如此,但是还是强烈建议属性名也按照标识符的规范命名
属性值
- 对象的属性值可以是任意的数据类型,也可以是一个对象
in
运算符
-
用来检查对象中是否含有某个属性
-
语法
attribute in obj
-
如果有返回
true
,没有返回false
let obj = Object()
obj.name = "Jack"
obj.age = 18
obj.gender = "Male"
let w = 'weight'
obj[w] = '60kg'
let symbl = Symbol()
obj[symbl] = 'Attribute added by Symbol'
console.log(obj)
//{name: 'Jack', age: 18, gender: 'Male', weight: '60kg', Symbol(): 'Attribute added by Symbol'}
let str = JSON.stringify(obj)
console.log(str)
//{"name":"Jack","age":18,"gender":"Male","weight":"60kg"}
console.log(obj[symbl]) //Attribute added by Symbol
console.log("name" in obj)
对象字面量
-
可以直接使用{} 来创建对象
-
使用{}所创建的对象,可以直接向对象中添加属性
-
语法:
{ 变量, 属性名:属性值, [属性名/变量]:属性值, "属性名":属性值 }
let mySymbol = Symbol()
let name = "Jack"
let attr = "str"
let obj = {
name,
age:18,
["gender"]:"男",
[mySymbol]:"特殊的属性",
innerObj:{
a:1,
b:true
},
[attr]:"I'm Jack",
"Tom":"Hello Tom."
}
let str=JSON.stringify(obj)
console.log(str)
//{"name":"Jack","age":18,"gender":"男","innerObj":{"a":1,"b":true},"str":"I'm Jack","Tom":"Hello Tom."}
枚举属性
for-in
语句
for(let Attribute in Object){}
并不是所有的属性都可以枚举,使用符号添加的属性不可枚举
let obj = {
name:'Jack',
age:18,
gender:"M",
address:"UK",
[Symbol()]:"Test Attr" // 符号添加的属性是不能枚举的
}
for(let propName in obj){
console.log(propName, obj[propName])
}
/*
name Jack
age 18
gender M
address UK
*/
可变类型
对象属于可变类型
对象创建完成后,可以任意的添加删除修改对象中的属性
注意:
- 当对两个对象进行相等或全等比较时,比较的是对象的内存地址
- 如果有两个变量同时指向一个对象,通过一个变量修改对象时,对另外一个变量也会产生影响
const
声明只是禁止变量被重新赋值,对对象的属性修改没有任何影响
let obj = {name:"Jack", age:"18"}
let obj2 = Object()
let obj3 = Object()
console.log(obj2 == obj3) // false
let obj4 = obj
obj4.name = "Rose"
console.log(JSON.stringify(obj)) // {"name":"Rose","age":"18"}
console.log(obj === obj4) // true
方法
当一个对象的属性指向一个函数,那么我们就称这个函数是该对象的方法,调用函数就称为调用对象的方法
let obj = {
name:"Jack",
introduce(){
console.log("I am", this.name)
},
greet(name_){
console.log("Hello,", name_)
},
farewell:function(name_){
console.log("Goodbye,", name_)
}
}
obj.introduce() // I am Jack
obj.greet("Rose") // Hello, Rose
obj.farewell("Rosy") // Goodbye, Rosy
Window对象
-
在浏览器中,浏览器为我们提供了一个window对象,可以直接访问
-
window对象代表的是浏览器窗口,通过该对象可以对浏览器窗口进行各种操作,除此之外window对象还负责存储JS中的内置对象和浏览器的宿主对象
-
window对象的属性可以通过window对象访问,也可以直接访问
-
函数就可以认为是window对象的方法
7.函数
基本概念
-
函数也是一个对象
-
它具有其他对象所有的功能
-
函数中可以存储代码,且可以在需要时调用这些代码
-
使用
typeof
检查函数对象时会返回function
function fact(n){
let ret=1
for(let i=1;i<=n;i++)
ret*=i
return ret
}
console.log(fact(5)) // 120
函数的定义方式
-
函数声明
-
函数表达式
-
箭头函数
-
// 1 function func(){ } // 2 const var_ = function(){ } // 3 () => { }
function fn(){
console.log("函数声明所定义的函数~")
}
const fn2 = function(){
console.log("函数表达式")
}
const fn3 = () => {
console.log("箭头函数")
}
const fn4 = () => console.log("箭头函数")
console.log(typeof fn) //fn-fn4全是function
console.log(typeof fn2)
console.log(typeof fn3)
console.log(typeof fn4)
函数参数
无需声明变量类型,其他类似C语言
// 当箭头函数中只有一个参数时,可以省略()
const fn = a => {
console.log("a =", a);
}
// 定义参数时,可以为参数指定默认值
// 默认值,会在没有对应实参时生效
const fn2 = (a=10, b=20, c=30) => {
console.log("a =", a);
console.log("b =", b);
console.log("c =", c);
}
fn2(233,666) // a=233 b=666 c=30
对象作为参数,传递实参时,传递并不是变量本身,而是变量中存储的值
-
修改变量时,只会影响当前的变量
-
修改对象时,如果有其他变量指向该对象则所有指向该对象的变量都会受到影响
function changeName(obj){
obj.name = 'Not ' + obj.name
}
let obj = {name:'Jack'}
changeName(obj)
console.log(obj) // {name: 'Not Jack'}
函数作为参数,函数也是一个对象(一等函数),别的对象能做的事情,函数也可以
function fn(a){
console.log("type of a:", typeof a)
console.log("a =", a)
}
fn(function(){
console.log("我是匿名函数~")
})
fn(()=>console.log("我是箭头函数"))
/*
type of a: function
a = ƒ (){
console.log("我是匿名函数~")
}
type of a: function
a = ()=>console.log("我是箭头函数")
*/
函数的返回值
-
在函数中,可以通过return关键字来指定函数的返回值
-
任何值都可以作为返回值使用(包括对象和函数之类)
-
如果return后不跟任何值,则相当于返回undefined
-
如果不写return,那么函数的返回值依然是undefined
-
箭头函数的返回值可以直接写在箭头后,如果直接在箭头后设置对象字面量为返回值时,对象字面量必须使用()括起来
const sum = (a, b) => a + b
const fn = () => ({name:"Jack"})
作用域
作用域指的是一个变量的可见区域
作用域有两种:
-
全局作用域
- 全局作用域在网页运行时创建,在网页关闭时消耗
- 所有直接编写到script标签中的代码都位于全局作用域中
- 全局作用域中的变量是全局变量,可以在任意位置访问
-
局部作用域
-
块作用域是一种局部作用域
-
块作用域在代码块执行时创建,代码块执行完毕它就销毁
-
在块作用域中声明的变量是局部变量,只能在块内部访问,外部无法访问
-
函数作用域也是一种局部作用域
-
函数作用域在函数调用时产生,调用结束后销毁
-
函数每次调用都会产生一个全新的函数作用域
-
在函数中定义的变量是局部变量,只能在函数内部访问,外部无法访问
-
作用域链:
-
当我们使用一个变量时,JS解释器会优先在当前作用域中寻找变量
-
如果找到了则直接使用,如果没找到,则去上一层作用域中寻找,找到了则使用
-
如果还没找到,则继续去上一层寻找,以此类推
-
如果一直到全局作用域都没找到,则报错
xxx is not defined
立即执行函数(IIFE)
-
立即是一个匿名的函数,并它只会调用一次
-
可以利用IIFE来创建一个一次性的函数作用域,避免变量冲突的问题
(function(){
let a = 10
console.log(111)
})(); //此处自动添加分号机制不起作用,注意需要分号
(function(){
let a = 20
console.log(222)
}())
// 111
// 222
this
- 函数在执行时,JS解析器每次都会传递进一个隐含的参数,这个参数就叫做
this
this
会指向一个对象,this
所指向的对象会根据函数调用方式的不同而不同- 以函数形式调用时,
this
指向的是window
- 以方法的形式调用时,
this
指向的是调用方法的对象
- 以函数形式调用时,
- 通过
this
可以在方法中引用调用方法的对象
function fn() {
console.log(this === window)
console.log(this)
}
const obj1 = {name:'Jack'}
obj1.inFun = fn
const obj2 = {name:'Rose', inFun:fn}
const obj3 = {
name:'Tom',
inFun:function(){
console.log(this, this.name)
}
}
window.fn() // true Window {...}
obj1.inFun() // false {name: 'Jack', inFun: ƒ}
obj2.inFun() // false {name: 'Rose', inFun: ƒ}
obj3.inFun() // {name: 'Tom', inFun: ƒ} 'Tom'
- 箭头函数没有自己的
this
,它的this
由外层作用域决定,箭头函数的this
和它的调用方式无关
function fn1() {
console.log(this)
}
const fn2 = () => console.log(this)
const obj = {
name:'Jack',
fn1, // fn1:fn1
fn2,
inFun(){ // inFun:function(){
console.log(this)
const inFun2 = () => {
//这里的this和函数外的this相同
console.log('In Infun',this)
}
inFun2()
}
}
fn2() // Window {...}
obj.fn1() // {name: 'Jack', fn1: ƒ, fn2: ƒ, inFun: ƒ}
obj.fn2() // Window {...}
obj.inFun()
/*
{name: 'Jack', fn1: ƒ, fn2: ƒ, inFun: ƒ}
In Infun {name: 'Jack', fn1: ƒ, fn2: ƒ, inFun: ƒ}
*/
高阶函数
如果一个函数的参数或返回值是函数,则这个函数就称为高阶函数
将函数作为参数,意味着可以对另一个函数动态的传递代码
function someFn() {
return "hello"
}
function outer(cb){
return () => {
console.log("记录日志~~~~~")
const result = cb()
return result
}
}
let result = outer(someFn)
console.log(result)
function test(){
console.log("test~~~~")
return "test"
}
let newTest = outer(test)
console.log("A ",newTest)
console.log("B ",newTest())
闭包
闭包就是能访问到外部函数作用域中变量的函数
当我们需要隐藏一些不希望被别人访问的内容时就可以使用闭包
构成闭包的要件:
- 函数的嵌套
- 内部函数要引用外部函数中的变量
- 内部函数要作为返回值返回
function outer(){
let num = 0 // 位于函数作用域中
return () => { //返回值是undefined
num++
console.log(num)
}
}
const newFn = outer()
const Fn2 = outer()
newFn() // 1
newFn() // 2
Fn2() // 1
newFn() // 3
Fn2() // 2
console.log(newFn)
/*
() => { //返回值是undefined
num++
console.log(num)
}
*/
console.log(newFn())
// 4
// undefined
闭包的生命周期:
-
闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
-
在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)
注意事项:
- 闭包主要用来隐藏一些不希望被外部访问的内容,这就意味着闭包需要占用一定的内存空间
- 相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能),需要执行次数较少时,使用闭包,需要大量创建实例时,使用类
递归
类似C语言
可变参数
-
arguments
是函数中又一个隐含参数 -
arguments
是一个类数组对象(伪数组),和数组相似,可以通过索引来读取元素,也可以通过for
循环变量,但是它不是一个数组对象,不能调用数组的方法 -
arguments
用来存储函数的实参,无论用户是否定义形参,实参都会存储到arguments
对象中,可以通过该对象直接访问实参
function f1(){
console.log(arguments)
}
f1() // Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
f1('Jack', 1914) // Arguments(2) ['Jack', 1914, callee: ƒ, Symbol(Symbol.iterator): ƒ]
function sum(a,b){
let ret = 0
console.log(a,b)
for(let i=0; i<arguments.length; i++){
console.log(arguments[i])
ret += arguments[i]
}
for(let v of arguments)
console.log(v)
return ret
}
sum(233,66)
console.log('sum =',sum(3,7,8,9,10))
可变参数,在定义函数时可以将参数指定为可变参数
- 可变参数可以接收任意数量实参,并将他们统一存储到一个数组中返回
- 可变参数的作用和
arguments
基本是一致,但是也具有一些不同点:- 可变参数的名字可以自己指定
- 可变参数就是一个数组,可以直接使用数组的方法
- 可变参数可以配合其他参数一起使用
function sum(...arr){
return num.reduce((a,b)=>a+b,0)
}
function func(a,b,...args){
for(let v of args)
console.log(v)
console.log(args)
}
console.log(sum(1,2,3,4)) // 10
console.log(func(11,22,33,44,55))
/*
33
44
55
[33, 44, 55]
*/
call & apply
根据函数调用方式的不同,this
的值也不同:
- 以函数形式调用,
this
是window
- 以方法形式调用,
this
是调用方法的对象 - 构造函数中,
this
是新建的对象 - 箭头函数没有自己的
this
,由外层作用域决定 - 通过
call
和apply
调用的函数,它们的第一个参数就是函数的this
函数.call()
函数.apply()
call
和apply
除了可以调用函数,还可以用来指定函数中的this
call
和apply
的第一个参数,将会成为函数的this
- 通过
call
方法调用函数,函数的实参直接在第一个参数后一个一个的列出来 - 通过
apply
方法调用函数,函数的实参需要通过一个数组传递
function fn(arg1,arg2){
console.log(arg1,arg2)
console.log('Fn this is',this)
}
const obj = {
name:'Jack',
fn
}
fn.call(obj,'abc',233) // this 是 obj
fn.apply(console,[233,'abc']) // this 是 console
bind
通过bind
返回的函数,this
由bind
第一个参数决定(无法修改)
bind()
是函数的方法,可以用来创建一个新的函数
bind
可以为新函数绑定this
bind
可以为新函数绑定参数
箭头函数没有自身的this
,它的this
由外层作用域决定,也无法通过call apply
和bind
修改它的this
箭头函数中没有arguments
function func(a,b){
console.log(a,b)
console.log(this)
}
const obj1 = {name:'Jack'}
const obj2 = {name:'Rose'}
const f1 = func.bind(obj1)
const f2 = func.bind(obj2,929,676)
f1() // undefined undefined {name:'Jack'}
f1(114,514) // 114 514 {name:'Jack'}
f2() // 929 676 {name:'Rose'}
f2(114,514) // 929 676 {name:'Rose'}
8.面向对象
使用Object创建对象的问题:
-
无法区分出不同类型的对象
-
不方便批量创建对象
类
-
类是对象模板,可以将对象中的属性和方法直接定义在类中定义后,就可以直接通过类来创建对象
-
通过同一个类创建的对象,我们称为同类对象,可以使用
instanceof
来检查一个对象是否是由某个类创建,如果某个对象是由某个类所创建,则我们称该对象是这个类的实例 -
类的定义
class 类名 {} // 类名要使用大驼峰命名 const 类名 = class {}
-
通过类创建对象
new 类()
类的属性
类的方法
类的构造函数
-
在类中可以添加一个特殊的方法
constructor
-
该方法我们称为构造函数(构造方法)
-
构造函数会在我们调用类创建对象时执行
class Person{
// 类的代码块,默认就是严格模式,
// 类的代码块是用来设置对象的属性的,不是什么代码都能写
constructor(argX, argY){
this.argX = argX
this.argY = argY
}
arg1 //没赋值的实例属性,默认值为undefined
arg2
name = "Jack" // Person的实例属性name
age = 18 // 实例属性只能通过实例访问
static test = "test静态属性" // 使用static声明的属性,是静态属性(类属性) Person.test
introduce(){
console.log('I am',this.name)
}
static testFn(){
console.log('I\'m static')
console.log(this)
}
}
const p1 = new Person()
const p2 = new Person('Rose', 233)
console.log(p1)
// Person {arg1: undefined, arg2: undefined, name: 'Jack', age: 18, argX: undefined, …}
console.log(JSON.stringify(p1))
// {"name":"Jack","age":18}
console.log(p2)
// Person {arg1: undefined, arg2: undefined, name: 'Jack', age: 18, argX: 'Rose', …}
console.log(JSON.stringify(p2))
// {"name":"Jack","age":18,"argX":"Rose","argY":233}
console.log(Person.test) // test静态属性
p1.introduce() // I am Jack
Person.testFn()
// I'm static
/*
class Person{
// 类的代码块,默认就是严格模式,
...
*/
封装
-
对象就是一个用来存储不同属性的容器
-
对象不仅存储属性,还要负责数据的安全
-
直接添加到对象中的属性,并不安全,因为它们可以被任意的修改
-
如何确保数据的安全:
-
私有化数据,将需要保护的数据设置为私有,只能在类内部使用
-
提供
setter
和getter
方法来开放对数据的操,属性设置私有,通过getter setter
方法操作属性带来的好处:- 可以控制属性的读写权限
- 可以在方法中对属性的值进行验证
-
-
封装主要用来保证数据的安全
-
实现封装的方式:
- 属性私有化 加
#
- 通过
getter
和setter
方法来操作属性
- 属性私有化 加
class Person{
// 实例使用#开头就变成了私有属性,私有属性只能在类内部访问
// 私有属性需要先声明才能使用
#name
#age
constructor(name, age) {
this.#name = name
if(typeof age === 'number' && age>0)
this.#age = age
else this.#age = 0
}
introduce(){
console.log("I'm", this.name)
}
getName(){ // getter方法,用来读取属性
return this.#name
}
setName(name){ // setter方法,用来设置属性
this.#name = name
}
getAge(){
return this.#age
}
setAge(age){
if(typeof age === 'number' && age>0)
this.#age = age
else console.log('Not valid')
}
}
const p1 = new Person('Jack',18)
p1.setName('Rose')
p1.setAge(-11)
p1.setAge('11')
console.log(p1)
/*
Not valid
Not valid
Person {#name: 'Rose', #age: 18}
*/
多态
-
在JS中不会检查参数的类型,所以这就意味着任何数据都可以作为参数传递
-
要调用某个函数,无需指定的类型,只要对象满足某些条件即可
-
如果一个东西走路像鸭子,叫起来像鸭子,那么它就是鸭子
-
多态为我们提供了灵活性
class Man{
constructor(name){
this.name = name
}
}
function printName(obj){
str = 'Hello '
if(obj instanceof Man){
str = 'Hello Mr.'
}
console.log(str + obj.name)
}
const m = new Man('White')
printName(m) // Hello Mr.White
继承
-
可以通过
extends
关键来完成继承 -
当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)
-
继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
-
通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展
-
通过继承可以在不修改一个类的情况下对其进行扩展
-
OCP 开闭原则:程序应该对修改关闭,对扩展开放
class Animal{
constructor(name){
this.name = name
}
introduce(){
console.log("I am animal")
}
}
class Dog extends Animal{
// 在子类中,可以通过创建同名方法来重写父类的方法
introduce(){
console.log("I am dog, a kind of animal")
}
}
class Cat extends Animal{
// 重写构造函数
constructor(name, age){
// 重写构造函数时,构造函数的第一行代码必须为super()
super(name) // 调用父类的构造函数
this.age = age
}
introduce(){
// 调用一下父类的sayHello
super.introduce() // 在方法中可以使用super来引用父类的方法
console.log("Meow~ meow~")
}
}
const dog = new Dog('Goru')
const cat = new Cat('Diona',12)
dog.introduce() // I am dog, a kind of animal
cat.introduce() // I am animal Meow~ meow~
console.log(dog) // Dog {name: 'Goru'}
console.log(cat) // Cat {name: 'Diona', age: 12}
对象的结构
- 对象自身
- 直接通过对象所添加的属性,位于对象自身中
- 在类中通过
x = y
的形式添加的属性,位于对象自身中
- 原型对象(prototype)
- 对象中还有一些内容,会存储到其他的对象里(原型对象)
- 在对象中会有一个属性用来存储原型对象,这个属性叫做
__proto__
- 原型对象也负责为对象存储属性
- 当我们访问对象中的属性时,会优先访问对象自身的属性
- 对象自身不包含该属性时,才会去原型对象中寻找
- 会添加到原型对象中的情况:
- 在类中通过
xxx(){}
方式添加的方法,位于原型中 - 主动向原型中添加的属性或方法
- 在类中通过
- 访问一个对象的原型对象
对象.__proto__
Object.getPrototypeOf(对象)
-
原型对象中的数据:
- 对象中的数据(属性、方法等)
- constructor (对象的构造函数)
-
原型链
-
读取对象属性时,会优先读取对象自身属性,如果对象中有,则使用,没有则去对象的原型中寻找
-
如果原型中有,则使用,没有则去原型的原型中寻找直到找到Object对象的原型(Object的原型没有原型(为null))
-
如果依然没有找到,则返回undefined
-
所有的同类型对象它们的原型对象都是同一个,也就意味着,同类型对象的原型链是一样的
-
Person对象的原型链:p对象 --> 原型 --> 原型 --> null Object对象的原型链:obj对象 --> 原型 --> null
-
class Person{
name
introduce(){
console.log('I am',this.name)
}
}
class Man extends Person{
weight
}
const p = new Person()
const m = new Person()
console.log(Object.getPrototypeOf(p) === p.__proto__) // true
console.log(p.__proto__) // Object { f introdce() ...}
console.log(p.__proto__.__proto__) // Object { f Object() ...}
console.log(p.__proto__.__proto__.__proto__) // null
console.log(m.__proto__)
/*
{constructor: ƒ, introduce: ƒ}
constructor: class Person
introduce: ƒ introduce()
[[Prototype]]: Object
*/
const obj = new Object() // obj = {}
console.log(obj.__proto__) // Object { f Object() ...}
console.log(obj.__proto__.__proto__) // null
除了通过__proto__
能访问对象的原型外,还可以通过类的prototype
属性,来访问实例的原型
class Person{
name
}
Person.prototype.introduce = () => {
console.log("It is person")
}
class Dog {}
const p1 = new Person()
const p2 = new Person()
console.log(p1.__proto__)
console.log(Person.prototype)
console.log(p.__proto__ === Person.prototype) // true
// 通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法 不要这么做
// p.__proto__.run = () => {
// console.log('我在跑~')
// }
// p.__proto__ = new Dog() // 直接为对象赋值了一个新的原型 不要这么做
instanceof
用来检查一个对象是否是一个类的实例
instanceof
检查的是对象的原型链上是否有该类实例
只要原型链上有该类实例,就会返回true
Object
是所有对象的原型,所以任何和对象和Object
进行instanceof
运算都会返回true
class Animal {}
class Dog extends Animal {}
const dog = new Dog()
const ani = new Animal()
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
console.log(dog instanceof Object) // true
console.log(ani instanceof Dog) // false
in
使用in
运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true
对象.hasOwnProperty(属性名)
(不推荐使用) 用来检查一个对象的自身是否含有某个属性
Object.hasOwn(对象, 属性名)
用来检查一个对象的自身是否含有某个属性
class Person{
name
func(){}
}
const p = new Person()
console.log("name" in p) // true
console.log("func" in p) // true
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")) // true
console.log(Object.hasOwn(p, "func")) //fasle
console.log(Object.hasOwn(p.__proto__, "func")) //true
*旧类
早期JS
中,直接通过函数来定义类
-
一个函数如果直接调用
Xxx()
那么这个函数就是一个普通函数 -
一个函数如果通过
new
调用new Xxx()
那么这个函数就是一个构造函数
//用立即函数包起来
var Person = (function () {
function Person(name, age){
// 在构造函数中,this表示新建的对象
this.name = name
this.age = age
// this.sayHello = function(){
// console.log(this.name)
// }
}
// 向原型中添加属性(方法)
Person.prototype.sayHello = function () {
console.log(this.name)
}
// 静态属性
Person.staticProperty = "xxx"
// 静态方法
Person.staticMethod = function () {}
return Person
})()
const p = new Person("Jack", 18)
// console.log(p)
var Animal = (function(){
function Animal(){
}
return Animal
})()
var Cat = (function(){
function Cat(){
}
// 继承Animal
Cat.prototype = new Animal()
return Cat
})()
var cat = new Cat()
console.log(cat)
new运算符
当使用new
去调用一个函数时,这个函数将会作为构造函数调用,使用new调用函数时,将会发生这些事:
- 创建一个普通的JS对象(
Object对象 {}
), 为了方便,称其为新对象 - 将构造函数的
prototype
属性设置为新对象的原型 - 使用实参来执行构造函数,并且将新对象设置为函数中的
this
- 如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回(千万不要这么做)如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回
通常不会为构造函数指定返回值
function MyClass(){
// var newInstance = {}
// newInstance.__proto__ = MyClass.prototype
}
var mc = new MyClass()
console.log(mc)
/*
MyClass{
[[Prototype]]: Object
}
*/
9.数组
基本介绍
-
数组也是一种复合数据类型,在数组可以存储多个不同类型的数据
-
数组中存储的是有序的数据,数组中的每个数据都有一个唯一的索引,可以通过索引来操作获取数据
-
数组中存储的数据叫做元素
-
索引(index)是一组大于0的整数
-
创建数组,可以通过
Array()
来创建数组,也可以通过[]
来创建数组 -
向数组中添加元素
语法:
数组[索引] = 元素
-
读取数组中的元素
语法:
数组[索引]
- 如果读取了一个不存在的元素,不好报错而是返回undefined
-
length
- 获取数组的长度
- 获取的实际值就是数组的
最大索引 + 1
- 向数组最后添加元素:
数组[数组.length] = 元素
length
是可以修改的
const arr = [1, 2, 3, 4]
const arr2 = new Array()
arr2[0] = 1
arr2[1] = 'Jack'
arr2[2] = () => {}
arr2[3] = {name:'Jack'}
console.log(arr)
console.log(arr2)
console.log(arr.length)
遍历数组
let arr = [1,14,5,1,4]
for(let i=0; i<arr.length; i++){
console.log(arr[i])
}
for(let val of arr){
console.log(val)
}
数组的方法
Array.isArray()
- 用来检查一个对象是否是数组
array.at(index)
- 可以根据索引获取数组中的指定元素
at
可以接收负索引作为参数arr.at(-i) === arr[arr.length-i]
array.concat(array...)
- 用来连接两个或多个数组
- 非破坏性方法,不会影响原数组,而是返回一个新的数组
console.log(Array.isArray({name:'Jack'})) // false
console.log(Array.isArray([1,2,3])) // true
const arr = ['Jack', 'Rose', 'Tom', 'Jerry']
console.log(arr.at(1)) // Rose
console.log(arr.at(-2)) // Tom
const arr2 = ['杰克', '汤姆']
let tmp = arr.concat(arr2)
console.log(tmp) // ['Jack', 'Rose', 'Tom', 'Jerry', '杰克', '汤姆']
tmp = arr.concat([1,2], arr2, [{name:'Jcak'}])
console.log(tmp) // ['Jack', 'Rose', 'Tom', 'Jerry', 1, 2, '杰克', '汤姆', {…}]
-
array.indexOf(element,postion)
- 获取元素在数组中第一次出现的索引
- 参数:要查询的元素,查询的起始位置
-
array.lastIndexOf(element,postion)
- 获取元素在数组中最后一次出现的位置
- 返回值:找到了则返回元素的索引,没有找到返回-1
-
array.join(operator)
- 将一个数组中的元素连接为一个字符串
- 参数:指定一个字符串作为连接符
-
array.slice(start, end)
- 用来截取数组(非破坏性方法)
- 参数:截取的起始位置(包括该位置),截取的结束位置(不包括该位置,可以是负值)
- 如果将两个参数全都省略,则可以对数组进行浅拷贝(浅复制)
let arr = [1,2,3,4,3]
let pos = arr.indexOf(3)
console.log(pos) // 2
pos = arr.indexOf(3,3)
console.log(pos) // 4
pos = arr.lastIndexOf(3)
console.log(pos) // 4
pos = arr.lastIndexOf(3,3)
console.log(pos) // 2
let str = arr.join(' + ')
console.log(str) // 1 + 2 + 3 + 4 + 3
let a = arr.slice(0,2)
console.log(a) // [1, 2]
a = arr.slice(2)
console.log(a) // [3, 4, 3]
a = arr.slice(1,-1)
console.log(a) // [2, 3, 4]
a = arr.slice()
console.log(a) // [1, 2, 3, 4, 3]
array.push(...element)
- 向数组的末尾添加一个或多个元素,并返回新的长度
array.pop()
- 删除并返回数组的最后一个元素
array.unshift(...element)
- 向数组的开头添加一个或多个元素,并返回新的长度
array.shift()
- 删除并返回数组的第一个元素
array.splice(start, number, ...element)
- 可以删除、插入、替换数组中的元素
- 参数:删除的起始位置,删除的数量,要插入的元素
- 返回值:返回被删除的元素
array.reverse()
- 反转数组
array.sort(function(arg1,arg2))
- sort用来对数组进行排序(会对改变原数组)
- sort默认会将数组升序排列(sort默认会按照Unicode编码进行排序,所以如果直接通过sort对数字进行排序,可能会得到一个不正确的结果)
- 参数:可以传递一个回调函数作为参数,通过回调函数来指定排序规则,回调函数返回值为负数,不改变两个变量顺序,为正数,改变两变量顺序,
(a, b) => a - b
升序,(a, b) => b - a
降序
array.forEach(function(element,index,array))
- 用来遍历数组
- 它需要一个回调函数作为参数,这个回调函数根据数组中元素数量被调用多次,每次调用,都会将数组中的数据作为参数传递
array.filter(function(element))
- 将数组中符合条件的元素保存到一个新数组中返回
- 需要一个回调函数作为参数,会为每一个元素去调用回调函数,并根据返回值来决定是否将元素添加到新数组中
- 非破坏性方法,不会影响原数组
array.map(function(element))
- 根据当前数组生成一个新数组
- 需要一个回调函数作为参数,回调函数的返回值会成为新数组中的元素
- 非破坏性方法不会影响原数组
array.reduce(function(arg1,arg2), init_val)
- 可以用来将一个数组中的所有元素整合为一个值
- 参数:回调函数,通过回调函数来指定合并的规则,可选参数,初始值
const arr1 = [0,1,2,3,4]
console.log(arr1.splice(1,3,['Jack','Rose'])) // [1,2,3]
console.log(arr1) // [0, Array(2), 4]
const arr2 = [1,14,51,4,23,3]
arr2.sort()
console.log(arr2) // [1, 14, 23, 3, 4, 51]
arr2.sort((a,b)=>b-a)
console.log(arr2) // [51, 23, 14, 4, 3, 1]
const arr3 = [1,2,3,4]
arr3.forEach((a,i,arr)=>{
arr[i] = a*a
})
console.log(arr3) // [1, 4, 9, 16]
const arr4 = ['Jack', 'Jacky', 'Tom', 'Jerry', 'Mike']
let tmp1 = arr4.filter( val=>{
return val[0] === 'J'
})
console.log(tmp1) // ['Jack', 'Jacky', 'Jerry']
const arr4 = [1,4,9,16,233]
let tmp2 = arr4.map( val=>{
return val**0.5
})
console.log(tmp2) // [1, 2, 3, 4, 15.264337522473747]
const arr5 = [2,4,6,8,10]
let tmp3 = arr5.reduce((a,b)=>{
return a+b
},10000)
console.log(tmp3) // 10030
对象的复制
复制必须要产生新的对象
当调用slice
时,会产生一个新的数组对象,从而完成对数组的复制
const arr = [0,1,2,3]
const arr2 = arr // 不是复制
arr[0] = 'changed'
console.log(arr2) // ['changed', 1, 2, 3]
const arr3 = arr.slice()
arr[1] = 'Jack'
console.log(arr3) // ['changed', 1, 2, 3]
console.log(arr === arr2) // true
console.log(arr === arr3) // false
- 深拷贝(shallow copy)
- 通常对对象的拷贝都是浅拷贝
- 浅拷贝顾名思义,只对对象的浅层进行复制(只复制一层)
- 如果对象中存储的数据是原始值,那么拷贝的深浅是不重要
- 浅拷贝只会对对象本身进行复制,不会复制对象中的属性(或元素)
- 深拷贝(deep copy)
- 深拷贝指不仅复制对象本身,还复制对象中的属性和元素
- 因为性能问题,通常情况不太使用深拷贝
const arr = [
{name:'Jack', age:18},
{name:'Rose', age:16}
]
const arr2 = arr.slice() // 浅拷贝
arr[0].name = 'Tom'
console.log(arr2[0].name) // Tom
const arr3 = structuredClone(arr) // 专门用来深拷贝的方法
arr[1].name = 'Jerry'
console.log(arr3[1].name) // Rose
...
(展开运算符)- 可以将一个数组中的元素展开到另一个数组中或者作为函数的参数传递
- 通过它也可以对数组进行浅复制
- 对象的复制
Object.assign(目标对象, 被复制的对象)
- 将被复制对象中的属性复制到目标对象里,并将目标对象返回
- 也可以使用展开运算符对对象进行复制
const arr = [0,1,2,3]
const arr2 = [...arr]
console.log(arr === arr2) // false
const obj = {name:'Jack', age:18}
const obj2 = {address:'US', age: 16}
Object.assign(obj2,obj)
console.log(obj2) // {address: 'US', age: 18, name: 'Jack'}
const obj3 = {name:'杰克', ...obj, age:28}
// 将obj中的属性在新对象中展开,相同名称的属性,后面的覆盖前面的
console.log(obj3) // {name: 'Jack', age: 28}