介绍
JS的this指向、函数方法和class类的内容
# JS中this的用法
this
是 JavaScript 语言的一个关键字。
它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。
function test() {
this.x = 1;
}
上面代码中,函数test
运行时,内部会自动有一个this
对象可以使用。
那么,this
的值是什么呢?
函数的不同使用场合,this
有不同的值。总的来说,this
就是函数运行时所在的环境对象。下面分四种情况,详细讨论this
的用法。
# 情况一:纯粹的函数调用
这是函数的最通常用法,属于全局性调用(window对象的一个方法),因此this
就代表全局对象。请看下面这段代码,它的运行结果是1。
var x = 1;
function test() {
console.log(this.x);
}
test(); // 1
# 情况二:作为对象方法的调用
函数还可以作为某个对象的方法调用,这时this
就指这个上级对象。
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m(); // 1
# 情况三: 作为构造函数调用
所谓构造函数,就是通过这个函数,可以生成一个新对象。这时,this
就指这个新对象(自身实例)。
function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1
运行结果为1。为了表明这时this不是全局对象,我们对代码做一些改变:
var x = 2;
function test() {
this.x = 1;
}
var obj = new test();
x // 2
运行结果为2,表明全局变量x
的值根本没变。
# 情况四: apply 调用
apply()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this
指的就是这第一个参数。
var x = 0;
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply() // 0
apply()
的参数为空时,默认调用全局对象。因此,这时的运行结果为0
,证明this
指的是全局对象。
如果把最后一行代码修改为
obj.m.apply(obj); //1
运行结果就变成了1
,证明了这时this
代表的是对象obj
。
# 构造函数new 和 普通函数
- 了解
this
关键字后 我们再来了解一下 普通函数和构造函数 - 构造函数是普通函数中的一种,特点是可以用来构造实例对象继承属性和方法 构造函数适合模块化开发 可以使用
class
而普通函数适合单独一个功能的开发(比如声明一个简单处理的方法) - 构造函数建议用变量来储存 因为它的引用没有存储在任何地方
- 注意: 构造函数不可以使用箭头函数 普通函数可以用箭头函数
- 箭头函数不具备自身实例的
this
他的this
是上一级(父级)的实例 如果没有父级 指向的是window
全局 在构造函数中 this需要指向到自身实例 所以不能用箭头函数
- 箭头函数不具备自身实例的
# 构造函数和普通函数的区别
命名的方法不同
- 普通函数使用的是小驼峰命名法,而构造函数使用的是大驼峰命名法,即首字大小写的区别
// 构造函数
const p = new Person('lhm', 23, '女')
// 普通函数
person('lhm', 23, '女')
调用方式的不同
- 任何函数只要使用
new
关键字来调用,那么它就是一个构造函数 - 而如果不使用
new
关键字来调用,那么它就是一个普通函数
// 普通函数
function demo() {
console.log('我是普通函数')
}
// 使用普通函数
demo()
// 构造函数
function Structure() {
console.log('我是构造函数')
}
// 使用构造函数
// 使用构造函数 构造函数建议用变量来储存 因为它的引用没有存储在任何地方
const Demos = new Structure()
this
关键字指向不同
- 构造函数内部可以使用
this
关键字;普通函数内部不建议使用this
,因为这时候this
指向的是window
全局对象,这样无意间就会为window
添加了一些全局变量或函数- 在普通函数内部,
this
指向的是window
全局对象 - 在构造函数内部,
this
指向的是构造出来的新对象
- 在普通函数内部,
// 普通函数
function demo(text) {
console.log(this.text) // 指向window 打印结果undefined
}
// 使用普通函数传参
demo('123')
// 构造函数
function Structure(text) {
// 构造函数的this指向是自身实例
this.myTest = text
console.log(this.myTest) // 指向构造函数自身实例myTest 打印结果123
}
// 使用构造函数传参
const Demos = new Structure('123')
return
返回结果的不同
- 普通函数不写
return
的情况下返回结果为undefined
,构造函数返回结果为构造函数的实例对象的内容- 普通函数需要
return
需要的值 - 构造函数会默认返回
this
,也就是自身实例对象 通过变量储存后 直接可以从变量中引用
- 普通函数需要
// 普通函数
function demo() {
const myTest = '你好我是普通函数'
return myTest
}
// 使用普通函数传参
const demos = demo()
// 查看普通函数的值
console.log(demos) // 你好我是普通函数
// 构造函数
function Structure() {
const myTest = '你好我是构造函数'
// 给否早函数声明一个变量
this.myTest = myTest
}
// 使用构造函数传参
const Demos = new Structure()
// 查看构造函数的值
console.log(Demos.myTest) // 你好我是构造函数
继承性
- 这个是构造函数比普通函数最大的差距 构造函数更加的面向对象 可以使用
class
类 而普通函数不能使用class
# 构造函数的优缺点
- 构造函数最强大的特点是 可以使用
class
类的内容 实现模块化 继承等... 让构造函数更加的面向对象
构造函数的优点
- 我们先来举个例子:当我们要创建一些拥有共同属性的对象时,要做以下操作
const p1 = { name: 'zs', age: 6, gender: '男', hobby: 'basketball' }
const p2 = { name: 'ls', age: 6, gender: '女', hobby: 'dancing' }
const p3 = { name: 'ww', age: 6, gender: '女', hobby: 'singing' }
const p4 = { name: 'zl', age: 6, gender: '男', hobby: 'football' }
- 这样子很麻烦,代码冗杂耦合度高 当我们使用构造函数时:
function Person(name, gender, hobby) {
this.name = name
this.gender = gender
this.hobby = hobby
this.age = 6 //默认值为6
}
const p1 = new Person('zs', '男', 'basketball')
const p2 = new Person('ls', '女', 'dancing')
const p3 = new Person('ww', '女', 'singing')
const p4 = new Person('zl', '男', 'football')
- 只需要
new
一个新对象,实例对象会继承构造函数中的属性,直接传参,显然方便多了
构造函数的缺点
- 每创建一个新构造函数的对象都会创建一块内存空间导致内存的浪费 但是呢我们可以通过
class
类来弥补啊 通过class
的继承属性来完善构造函数 - 构造函数 不能用箭头函数!
# 判断构造函数是否在变量中
- 可以通过instanceof (opens new window) 运算符用于检测构造函数的
prototype
属性是否出现在某个变量(实例对象)的原型链上。
function Person(name) {
this.name = name
}
const p = new Person('Joe')
console.log(p instanceof Person) // true
# JS中class类
- 在 ES6 规范中,引入了
class
的概念。使得 JS 开发者终于告别了,直接使用原型对象模仿面向对象中的类和类继承时代。class
类的概念十分优秀 可以把他看成Vue框架的组件 声明一个类 可以再多个方法中引用 也可以被其他类进行继承class
类的所有方法都定义在类的prototype
属性上面。class
类的方法内部如果含有this
,它默认指向类的实例对象
class
是语法糖 其原理是通过: 函数+原型模拟出来的 并且他的类型就是构造函数new
- 构造函数 (opens new window)名首字母一般大写 所以
class
类也是首字母大写(双驼峰),普通函数名首字母一般小写(双驼峰)
- 构造函数 (opens new window)名首字母一般大写 所以
- 注意:
class
是构造函数 必须用new
构造函数来执行 普通函数是无法使用class
类的- 构造函数的this指向为自身实例 而普通函数this是指向全局
window
- 构造函数的this指向为自身实例 而普通函数this是指向全局
class Demo {
constructor(text, other) {
this.text = text
this.other = other
}
}
/* ✓ GOOD */
const ret = new Demo('你好', 'hello')
/* ✗ BAD */
const ret = Demo('你好', 'hello') // 普通函数不能使用class 必须是构造函数才可以
console.log(ret)
- 以下代码会介绍
class
中的方法
# 声明class
- 单纯声明
class
不写内容那么class
是一个空实例 class
声明后 可以使用class
外的值 属于同作用域
class Flame {
// 我是空实例
}
- 在声明的时候 声明默认值 也可以使用
constructor
定义变量- constructor (opens new window) 构造函数属于被实例化的特定类对象 (opens new window) 可以在里面生成该构造函数的变量 支持私有变量
class Type {
name = '你好'
constructor() {
this.color = 'blue'
this.age = 2
}
}
const ret = new Type()
console.log(ret) // Type {name: '你好', color: 'blue', age: 2}
ES2022
支持我们在constructor
外定义变量 需要参数的依旧需要在constructor
中定义, 两种都是定义实例化的方式- 建议需要参数的实例化存放在
constructor
中 - 不需要参数内部声明的实例化 放在
constructor
外 声明时候不需要this
- 建议需要参数的实例化存放在
class Type {
// 需要参数的需要在constructor内声明
constructor(old) {
this.age = old
}
// 不需要参数的在constructor外声明
name = '你好'
color = 'blue'
}
const ret = new Type()
console.log(ret) // Type {name: '你好', color: 'blue', age: 2}
- 通过哈希
#
前缀定义类私有域 (opens new window) 设置私有属性 以防止在类方法之外更改属性- 在
ES2022
之前,并没有实际意义上的私有字段。大家形成一种默契,通常用下划线_name
开头的字段名来表示私有字段,但是这些字段还是可以手动更改的。 #
暂时只能用在class
中
- 在
class Type {
name = '你好'
// 通过#设置私有属性
#id = 123
}
const ret = new Type()
// 修改属性 会报错
ret.#id = 321
// 查看属性 会报错
console.log(ret.#id) // 不可访问 会被报错 Private field '#id' must be declared in an enclosing class
- 通过
#
方法也可以进行私有
class Type {
name = '你好'
#id = 123
// 声明私有方法
#noChange(value) {
this.name = 'hello'
}
// 声明一个普通方法
yesChange(value) {
this.name = 'hello'
}
}
const ret = new Type()
// 可以进行修改
ret.yesChange()
// 私有方法不可以修改 会报错
ret.#noChange() // Private field '#noChange' must be declared in an enclosing class
- 还可以将
getter/setter
设为私有,只需要给这些方法名称前面加#
即可:
class TimeTracker {
name = 'zhangsan'
project = 'blog'
#hours = 0 // 私有类字段
set #addHours(hour) {
this.#hours += hour
}
get #timeSheet() {
return `${this.name} works ${this.#hours || 'nothing'} hours on ${
this.project
}`
}
constructor(hours) {
this.#addHours = hours
console.log(this.#timeSheet)
}
}
let person = new TimeTracker(4) // zhangsan works 4 hours on blog
# 使用in
来判断某个对象是否拥有某个私有属性
- 判断某个class类是否拥有某个特定的私有属性,是通过
in
操作符来判断的。
class Car {
#color
hasColor() {
return #color in this
}
}
const car = new Car()
console.log(car.hasColor()) // true
# 实例对象和方法定义constructor
- 一个类的类体是一对花括号/大括号
{}
中的部分。这是你定义类成员的位置,如方法或构造函数。
创建class的实例对象
constructor
方法是一个特殊的方法,这种方法用于创建和初始化一个由class
创建的实例对象。- 一个类只能拥有一个名为
constructor
的特殊方法 - 如果没有显示定义该方法,会自动添加一个空的
constructor
方法 - 通过
new
生成对象实例时,自动调用该方法 constructor
方法默认返回实例对象,但可以指定返回另外一个对象通过return
如:return Object.create(null)
- 一个类只能拥有一个名为
class Type {
// 通过constructor创建class实例化对象
constructor(text, other) {
// class是构造函数 所以this的执行是实例本身
this.text = text
this.other = other
}
}
const ret = new Type('你好', 'hello')
console.log(ret) // Type {text: '你好', other: 'hello'}
创建class的实例方法
class
中声明方法 跟普通函数方法声明一致 无需通过this
直接写方法即可
// 定义一个空值
let changeNumber = null
class Type {
// 通过constructor创建class实例化对象
constructor(text, other) {
// class是构造函数 所以this的执行是实例本身
this.text = text
this.other = other
}
// 在class中定义一个方法 不需要this和普通方法一样 有值需要return
show = () => {
// 进行空值赋值
changeNumber = '小明'
// 返回要处理的数据
return this.text + this.other + changeNumber
}
}
const ret = new Type('你好', 'hello')
console.log(ret.show()) // 你好hello小明
# 继承属性 extends
class
可以通过extends
关键字进行继承- 如果想在子类中添加新的
constructor
实例对象 那么就需要使用super()
传入父类的参数- 因为子类创建自身的
constructor
实例对象的时候 会覆盖继承过来的父类的constructor
所以需要使用super
关键字 访问父级的实例化对象 否则会报错! - super() (opens new window)关键字也是一个语法糖原理: 通过apply() (opens new window)修改了子类的this指向 让其
this
指向父级的实例化对象 也就是说super()
后 子级的constructor
的this指向是父级实例而非自身实例(子级) - 注意,在使用
this
关键字之前,必须在子构造函数中执行super()
。调用super()
确保父构造函数初始化实例。super()
会继承父类的构造函数, 但是如果父类的constructor
有参数, 子类继承的时候, 需要再次绑定其参数(需要再次传参)
- 因为子类创建自身的
extends
继承后父级的方法也可以在子级中使用 但是注意方法名称 如果名称一样 子级的方法会覆盖父级的方法
// 创建一个父类
class Type {
constructor(text, other) {
this.text = text
this.other = other
}
show() {
return this.text + this.other
}
}
// 创建一个子类 继承其父类
class TypeSon extends Type {
constructor(text, other, Sontext) {
// 在子类的constructor中定义实例对象 需要先super()绑定父级实例对象 然后才能在子级中创建自身的实例对象
super(text, other)
// 子类独有的实例对象
this.Sontext = Sontext
}
// 在子级中创建方法 如果和父级方法名一样 父级方法会被子级方法替换
showSon() {
return this.text + this.other
}
}
const ret = new TypeSon('父亲的text', '父亲的other', '儿子的Sontext')
console.log(ret) // TypeSon {text: '父亲的text', other: '父亲的other', Sontext: '儿子的Sontext'}
- 子类继承父类后 使用的时候直接导入子类构造即可 因为父类不存在子类声明的构造函数 所以谁继承 就使用继承者
# 静态方法/静态属性 static
静态方法:是使用static
关键字修饰的方法,又叫类方法.属于类的,不属于对象, 在实例化对象之前就可以通过类名.方法名调用静态方法。
- 静态方法/属性被用于实现属于整个类的功能。它与具体的实例类无关(实例就是
this.
的内容)。
class StaticClass {
// 声明一个静态属性
static staticNumber = 1
// 声明一个静态方法
static staticMethod() {}
// 声明一个实例属性
instanceNumbere = 1
// 声明一个实例方法
instanceMethod() {
// 在类的实例方法中访问静态属性 不可以用this, 要用类名
// 在实例方法中可以访问静态属性
console.log(StaticClass.staticNumber)
// 在实例方法中可以访问静态方法
StaticClass.staticMethod()
}
}
- 静态方法/属性是无法访问非静态方法/属性的(实例类), 只能访问自身的静态类
- 如果在静态方法/属性中包含
this
关键字,这个this
指的是实例类(class中所有静态方法/属性含继承静态类)
- 如果在静态方法/属性中包含
class StaticClass {
// 声明一个静态属性
static staticNumber = 1
// 声明一个静态方法
static staticMethod() {
// 在类的静态方法中访问静态属性, 可以用this, this指向静态类
// 无法在静态类中访问实例类
console.log(this.staticNumber)
// 也可以用类名访问静态属性
console.log(StaticClass.staticNumber)
}
}
静态方法/属性可以被继承, 和实例类一样
静态属性设定初始化值的,会被默认被初始化为
undefined
class StaticClass {
// 声明一个静态属性
static staticNumber: number
// 声明一个静态方法
static staticMethod() {
// 在类的静态方法中访问静态属性
console.log(this.staticNumber) // undefined
}
}
- 静态方法/属性不能
new
构造, 但可以直接通过类来使用(跟普通方法使用一样)instanceMethod
是实例方法,staticMethod
是静态方法, 属性的使用方法也是一样
// 导入实例
import { CreatedCanvas } from './components/iphone_render'
// 使用实例方法, 需要先通过new来构造
const instance = new CreatedCanvas()
// 使用实例方法
instance.instanceMethod()
// 使用静态方法, 无需构造, 使用导入的类名即可
CreatedCanvas.staticMethod()
# 取值函数getter
和存值函数setter
get
关键字class
类的实例被访问时 用get
进行拦截 做一些逻辑操作 比如不可私有化判断get
不带任何参数
set
关键字class
类的实例被赋值(传参)或修改时set
可以进行拦截 做一些逻辑操作set
必须设置形参
get
和set
关键字不会被extends
所继承- 注意:
get
和set
不是方法 是一个属性 不要用()
进行调用
// 创建一个父类
class Type {
constructor(text, other) {
this.text = text
this.other = other
}
//class类的实例被赋值(传参)和修改时 用set进行拦截
set changeClass(value) {
// set必须有参数才可以
// 把传参和class实例进行拼接操作
this.text += value
}
//class类的实例被访问时 用get进行拦截
get changeClass() {
// set必须有参数才可以
// 返回一些数据或者提示
return console.log('class被访问啦')
}
}
const ret = new Type('父亲的text', '父亲的other')
// 给set关键传参
ret.changeClass = '被修改啦'
console.log(ret.text) // 父亲的text被修改啦
// 访问class实例时候会触发 get关键字
ret.changeClass // class被访问啦
- 可以将
getter/setter
设为私有,只需要给这些方法名称前面加#
即可:
class TimeTracker {
name = 'zhangsan'
project = 'blog'
#hours = 0 // 私有类字段
set #addHours(hour) {
this.#hours += hour
}
get #timeSheet() {
return `${this.name} works ${this.#hours || 'nothing'} hours on ${
this.project
}`
}
constructor(hours) {
this.#addHours = hours
console.log(this.#timeSheet)
}
}
let person = new TimeTracker(4) // zhangsan works 4 hours on blog
# class导出导入
class
类 支持es6的export
的导出导入class
作为导出的构造函数 极其灵活 一个类可以包含多个方法 导入调用的时候 只需要new
构造一下 就能使用class
类中的constructor
构造实例 可以通过解构的方式 指定传值(传参) 就跟普通方法解构传参一样
class导出导入例子
- 创建一个要导出的
class
类
// 创建类
class Getfire {
constructor(obj) {
// 通过解构 传递指定的参数
const { name } = obj
this.name = name // 储存传递的参数
}
// 方法1
granaryFlame () {
// ..... 一些逻辑
return this.name
}
// 方法2
getFire (granary) {
this.name = '芜湖' + this.name
return this.name
// ..... 一些逻辑
}
}
// 命名导出class类
export { Getfire }
// 也可以这样命名导出class类
export class Getfire {
... TODO
}
- 导入
class
类
// 导入构造函数
import { Getfire } from './demo.js'
// 声明class构造函数
const Flame = new Getfire({
name: '起飞', // 解构传递指定的参数
})
// 使用构造函数中的方法
Flame.granaryFlame() // 测试
// 使用构造函数中的方法2
Flame.getFire() // 芜湖起飞
// 查看构造函数实例化的对象
# class中 普通函数和箭头函数
- class中 箭头函数和普通函数的问题 一般构造函数内的方法建议用箭头函数的方式声明
在不单独调用构造函数中的方法
- 声明构造函数后 通过引用声明构造函数中的方法
class Person {
constructor () {
this.name = '章三'
}
handleClick1 = () => {
console.log(this.name)
return this
}
handleClick2 () {
console.log(this.name)
return this
}
}
let person = new Person()
console.log(person.handleClick1() === person) // true
console.log(person.handleClick2() === person) // true
通过解构 单独调用构造函数中的方法
- 声明构造函数后 通过解构 再单独调用构造函数中的方法
class Person {
constructor() {
this.name = '章三'
}
handleClick1 = () => {
console.log(this);
console.log(this.name)
return this
}
handleClick2 () {
console.log(this);
console.log(this.name)
return this
}
}
let person = new Person()
let { handleClick1, handleClick2 } = person
console.log(handleClick1() === person) // true
console.log(handleClick2() === person) // 报错: Uncaught TypeError: Cannot read properties of undefined (reading 'name')
问题:
类的方法内部如果含有
this
,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。上面代码中,handleClick2 方法中的
this
,默认指向 Person 类的实例。但是,如果将这个方法提取出来单独使用,this
会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以this
实际指向的是 undefined),从而导致找不到 name 属性而报错。
解决:
- 一个比较简单的解决方法是,在构造方法中绑定 this(比如传参),这样就不会找不到 name 属性了
- 另一种解决方法是使用箭头函数,如 handleClick1 的箭头函数写法。
# class中的async/await
es7提供的async/await (opens new window), 可以代替promise, 做一些日常异步处理, 在class中使用, 需要写在箭头函数()
前面
class Person {
// 这是一个异步方法
gltfLoader = new THREE.GLTFLoader()
// 在箭头函数的()前添加async 关键字
loadIphone = async () => {
const result = await gltfLoader.loadAsync('car/scene.gltf') // 这里的await是等待异步方法执行完毕
console.log(result) // 想要的异步数据
}
createScene = () => {
// 在需要异步数据的地方调用异步方法
this.loadIphone()
}
}
# class类的使用流程
class类的继承性让我们需要用上下文的方式来对待, 所以类的继承方式非常重要
用three.js渲染的步骤进行流程细分
第一层, three.js渲染的公共类, 一些依赖于three.js渲染的类型, 比如
renderer
,scene
,camera
等, 通过ts进行类型设置, 通过import type
可以进行ts的类型引入第二层, three.js方法类, 适合写一些渲染后的方法, 比如添加一些
mesh
模型, 修改camera
相机的位置, 做一些物体的移动交互效果等
- 第三层, three.js渲染的方法, 这里就是three.js进行构建层, 用来创建画布渲染webgl等最基础的渲染相关操作
# 参考文献
Javascript 的 this 用法 (opens new window)
ECMAScript6 怎么写 class? - JavaScript前端Web工程师 (opens new window)
JavaScript里面函数和构造函数的区别 (opens new window)