# 实现类型判断
# 挑战准备
新建一个 getType.js
文件,在文件里写一个名为 getType
的函数,并导出这个函数,如下图所示:
这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,函数代码如下:
function getType(target) { | |
// 补充代码 | |
} | |
module.exports = getType; |
# 挑战内容
请封装一个函数,能够以字符串的形式精准地返回数据类型。
要求返回的类型全部由小写字母组成。
示例:
输入 | 输出 |
---|---|
true | 'boolean' |
100 | 'number' |
'abc' | 'string' |
100n | 'bigint' |
null | 'null' |
undefined | 'undefined' |
Symbol('a') | 'symbol' |
[] | 'array' |
{} | 'object' |
function fn() {} | 'function' |
new Date() | 'date' |
/abc/ | 'regexp' |
new Error() | 'error' |
new Map() | 'map' |
new Set() | 'set' |
new WeakMap() | 'weakmap' |
new WeakSet() | 'weakset' |
# 注意事项
- 文件名、函数名不可随意更改。
- 文件中编写的函数需要导出,否则将无法提交通过。
# 实验介绍
# 知识点
- JavaScript 数据类型
- 判断 JavaScript 数据类型的几种方法
# 题解
正式开始讲解前,先放上最终答案镇楼,代码如下:
function getType(target) { | |
const type = typeof target; | |
if (type !== "object") { | |
return type; | |
} | |
return Object.prototype.toString | |
.call(target) | |
.replace(/^\[object (\S+)\]$/, "$1") | |
.toLocaleLowerCase(); | |
} |
# 题解分析
判断一个数据的类型,比较常用的有下面几种方式:
typeof
instanceof
Object.prototype.toString.call(xxx)
# typeof
判断一个数据的类型,用得最多的就是 typeof 操作符, 但是使用 typeof
常常会遇到以下问题:
- 无法判断
null
。 - 无法判断除了
function
之外的引用类型。
同学们可以打开云课实验环境,亲自去写一写代码,在实践中学习。
打开实验环境后,分别创建 index.html
和 index.js
文件,在 index.js
中写入如下代码。
// 可以判断除了 null 之外的基础类型。 | |
console.log(typeof true); // 'boolean' | |
console.log(typeof 100); // 'number' | |
console.log(typeof "abc"); // 'string' | |
console.log(typeof 100n); // 'bigint' | |
console.log(typeof undefined); // 'undefined' | |
console.log(typeof Symbol("a")); // 'symbol' | |
// 无法判断 null。 | |
console.log(typeof null); // 输出 'object',原因在文章末尾解释。 | |
// 无法判断除了 function 之外的引用类型。 | |
console.log(typeof []); // 'object' | |
console.log(typeof {}); // 'object' |
编写完成后,不要忘记在 index.html
中引入 index.js
文件。然后右键点击 index.html
文件,点击 Open with Live Server
,实验环境就会启动一个 Web 服务,供我们在外部访问。
在启动的 Web 服务的控制台里查看代码运行情况,如下图:
# instanceof
typeof
无法精确地判断引用类型,这时,可以使用 instanceof 运算符,如下代码所示:
console.log([] instanceof Array); // true | |
const obj = {}; | |
console.log(obj instanceof Object); // true | |
const fn = function () {}; | |
console.log(fn instanceof Function); // true | |
const date = new Date(); | |
console.log(date instanceof Date); // true | |
const re = /abc/; | |
console.log(re instanceof RegExp); // true |
但是 instanceof
运算符一定要是 判断对象实例的时候才是正确的 ,也就是说,它不能判断原始类型,如下代码所示:
const str1 = "qwe"; | |
const str2 = new String("qwe"); | |
console.log(str1 instanceof String); //false,无法判断原始类型。 | |
console.log(str2 instanceof String); // true |
有同学说,这不正好, typeof
可以判断原始类型, instanceof
可以判断引用类型,把它俩结合起来,就可以实现精准判断类型的 getType
函数了。
别忘了,还有个 null
得处理一下,它比较特殊,我们可以直接判断变量全等于 null
,如下代码所示:
function getType(target) { | |
// ... | |
if (target === null) { | |
return "null"; | |
} | |
// ... | |
} |
现在,判断原始类型和引用类型的思路都有了,接下来就是动手写代码的事,但是真的去写就会发现,使用 instanceof
操作符来判断类型返回的是 true
或者 false
,写起来会非常麻烦。
其实, instanceof
运算符本来是用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上的,只是刚好可以用来判断类型而已,所以在这里才会讨论它,实际上用它来判断类型代码写起来不是很方便。
这时,Object.prototype.toString 出场了,实际项目中要封装判断类型的工具函数一般都是用的它。
# Object.prototype.toString.call(xxx)
调用 Object.prototype.toString
方法,会统一返回格式为 [object Xxx]
的字符串,用来表示该对象(原始类型是包装对象)的类型。
需要注意的是,在调用该方法时,需要加上 call
方法(原因后文解释),如下代码所示:
// 引用类型 | |
console.log(Object.prototype.toString.call({})); // '[object Object]' | |
console.log(Object.prototype.toString.call(function () {})); // "[object Function]' | |
console.log(Object.prototype.toString.call(/123/g)); // '[object RegExp]' | |
console.log(Object.prototype.toString.call(new Date())); // '[object Date]' | |
console.log(Object.prototype.toString.call(new Error())); // '[object Error]' | |
console.log(Object.prototype.toString.call([])); // '[object Array]' | |
console.log(Object.prototype.toString.call(new Map())); // '[object Map]' | |
console.log(Object.prototype.toString.call(new Set())); // '[object Set]' | |
console.log(Object.prototype.toString.call(new WeakMap())); // '[object WeakMap]' | |
console.log(Object.prototype.toString.call(new WeakSet())); // '[object WeakSet]' | |
// 原始类型 | |
console.log(Object.prototype.toString.call(1)); // '[object Number]' | |
console.log(Object.prototype.toString.call("abc")); // '[object String]' | |
console.log(Object.prototype.toString.call(true)); // '[object Boolean]' | |
console.log(Object.prototype.toString.call(1n)); // '[object BigInt]' | |
console.log(Object.prototype.toString.call(null)); // '[object Null]' | |
console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]' | |
console.log(Object.prototype.toString.call(Symbol("a"))); // '[object Symbol]' |
有了上面的基础,我们就可以统一调用 Object.prototype.toString
方法来获取数据具体的类型,然后把多余的字符去掉即可,只取 [object Xxx]
里的 Xxx
。
不过使用 Object.prototype.toString
判断原始类型时,会进行装箱操作,产生额外的临时对象,为了避免这一情况的发生,我们也可以结合 typeof
来判断除了 null
之外的原始类型,于是最后的代码实现如下:
function getType(target) { | |
// 先进行 typeof 判断,如果是基础数据类型,直接返回。 | |
const type = typeof target; | |
if (type !== "object") { | |
return type; | |
} | |
// 如果是引用类型或者 null,再进行如下的判断,正则返回结果,注意最后返回的类型字符串要全小写。 | |
return Object.prototype.toString | |
.call(target) | |
.replace(/^\[object (\S+)\]$/, "$1") | |
.toLocaleLowerCase(); | |
} |
上面代码中从 [object Xxx]
里取出 Xxx
用到了 replace 方法和正则,关于正则这种 “八股文”,想要进阶,躲是躲不掉的,但是不建议同学们去硬啃,太枯燥,可以了解一些用得较多的方法和写法,知道大概的写法后,再结合类似这个网站这样的正则可视化去分析该怎么写。关于正则,具体的内容会放到课程后面讲字符串、正则相关面试题时介绍。
其实,上面的函数还可以换一种写法,代码如下:
// ... | |
return Object.prototype.toString | |
.call(target) | |
.match(/\s([a-zA-Z]+)\]$/)[1] // 这种写法也可以。 | |
.toLocaleLowerCase(); |
不用正则也可以,只要能实现功能,想怎么写都行,代码如下:
// ... | |
return Object.prototype.toString | |
.call(target) | |
.slice(8, -1) // 这种写法也可以。 | |
.toLocaleLowerCase(); |
不过我个人觉得第一种写法的可读性最高,所以先介绍的是第一种。
这个函数还调用了 toLocaleLowerCase() 方法,把返回结果统一变成小写,因为 Object.prototype.toString
返回的类型字符串格式不是统一的,有些是首字母大写,有些是帕斯卡形式(单词中间也有大写字母),不好记,全部返回小写就没这些烦恼了。
至此, getType
方法就写完了,这个工具函数非常实用,有了它,再也不用去业务代码里写冗长的判断类型的代码了,赶紧在实际项目中用起来吧。
最容易的写法是
function getType(target) { | |
return Object.prototype.toString | |
.call(target) | |
.slice(8, -1) // 这种写法也可以。 | |
.toLocaleLowerCase(); | |
} |
# 知识延伸
# 为什么要使用 call
解答一下上文留下的疑问,为什么要写成 Object.prototype.toString.call(xxx)
的形式来判断 xxx
的类型?
call 是函数的方法,是用来改变 this 指向的,用 apply 方法也可以。
如果不改变 this
指向为我们的目标变量 xxx
, this
将永远指向调用的 Object.prototype
,也就是原型对象,对原型对象调用 toString
方法,结果永远都是 [object Object]
,如下代码所示:
Object.prototype.toString([]); // 输出 '[object Object]' 不调用 call,this 指向 Object.prototype,判断类型为 Object。 | |
Object.prototype.toString.call([]); // 输出 '[object Array]' 调用 call,this 指向 [],判断类型为 Array | |
Object.prototype.toString(1); // 输出 '[object Object]' 不调用 call,this 指向 Object.prototype,判断类型为 Object。 | |
Object.prototype.toString.call(1); // 输出 '[object Number]' 调用 call,this 指向包装对象 Number {1},判断类型为 Number |
可以重写 Object.prototype.toString
方法,把 this
打印出来验证一下,代码如下所示:
// 重写 Object.prototype.toString 方法,只打印 this | |
Object.prototype.toString = function () { | |
console.log(this); | |
}; | |
// 引用类型 | |
Object.prototype.toString([]); // 输出 Object.prototype | |
Object.prototype.toString.call([]); // 输出 [] | |
// 原始类型 | |
Object.prototype.toString(1); // 输出 Object.prototype | |
Object.prototype.toString.call(1); // 输出 Number {1},这里的 Number {1},是一个包装类,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质 |
这下同学们清楚为什么该用 call 方法了吧,当然这里只是简单介绍下,如果同学们对于 this
和 call
还有疑问,也不用担心,后面的真题挑战里就有 “实现 call
方法” 这道题,我们到时再做详尽的介绍。
另外,关于数据类型,还有一些常见的面试题,总结在文章末尾,供同学们参考。
# 常见面试题 1:JavaScript 有哪些数据类型
答: JavaScript 的数据类型分为原始类型和对象类型。
原始类型有 7 种,分别是:
- Boolean
- Number
- BigInt
- String
- Null
- Undefined
- Symbol
对象类型(也称引用类型)是一个泛称,包括数组、对象、函数等一切对象。
# 常见面试题 2:typeof null 的结果是什么
答:
typeof null; // 'object' |
null
作为一个基本数据类型为什么会被 typeof
运算符识别为对象类型呢?
事实上,这是第一版 JavaScript 留下来的一个 bug。
JavaScript 中不同对象在底层都表示为二进制,而 JavaScript 中会把二进制前三位都为 0 的判断为 object
类型,而 null
的二进制表示全都是 0,自然前三位也是 0,所以执行 typeof
时会返回 'object'
。
那为啥那一堆设计语言的大佬们会放任这个 bug 存在这么多年呢?
因为这个 bug 牵扯了太多的 Web 系统,一旦改了,会产生更多的 bug,令很多系统无法工作,也许这个 bug 永远都不会修复了。
判断一个类型为 null
可以这么写,直接判断变量全等于 null
:
let a = null; | |
if (a === null) { | |
// do something | |
} |
# 常见面试题 3:原始类型和引用类型的区别是什么
答:
类型 | 原始类型 | 对象类型 |
---|---|---|
值 | 不可改变 | 可以改变 |
属性和方法 | 不能添加 | 能添加 |
存储值 | 值 | 地址(指针) |
比较 | 值的比较 | 地址的比较 |
# 常见面试题 4:typeof 和 instanceof 的区别是什么
答:
typeof
运算符用来判断数据的类型。instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上,也可以用来判断数据的类型。typeof
返回一个变量的类型字符串,instanceof
返回的是一个布尔值。typeof
可以判断除了null
以外的基础数据类型,但是判断引用类型时,除了function
类型,其他的无法准确判断。instanceof
可以准确地判断各种引用类型,但是不能正确判断原始数据类型。
# 常见面试题 5:Symbol 解决了什么问题
答: Symbol
是 ES6 时新增的特性, Symbol
是一个基本的数据类型,表示独一无二的值,主要用来防止对象属性名冲突问题。
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的属性,新属性的名字就有可能与现有属性的名字产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入 Symbol
的原因之一。
Symbol
值通过 Symbol
函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol
类型。凡是属性名属于 Symbol
类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
代码演示如下:
const obj = { | |
name: "lin", | |
age: 18, | |
}; | |
obj.name = "xxx"; // 给 obj.name 赋值,把以前的 name 覆盖了 | |
console.log(obj); // { name: 'xxx', age: 18 } |
const obj = { | |
name: "lin", | |
age: 18, | |
}; | |
const name = Symbol("name"); | |
obj[name] = "xxx"; // 使用 Symbol,不会覆盖 | |
console.log(obj); // { name: 'lin', age: 18, Symbol(name): 'xxx' } | |
console.log(obj.name); // 'lin' | |
console.log(obj[name]); // 'xxx' |
# 判断对象是否为空
# 挑战准备
新建一个 isEmptyObject.js
文件,在文件里写一个名为 isEmptyObject
的函数,并导出这个函数,如下图所示:
这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,函数代码如下:
function isEmptyObject(obj) { | |
// 补充代码 | |
} | |
module.exports = isEmptyObject; |
# 挑战内容
实现一个函数,判断传入的对象是否为空。如果对象为空返回 true,否则返回 false。
不必考虑传入原始类型的情况,本题测试用例中传入的参数都是对象类型。
示例:
输入:{} | |
输出:true |
输入:{ name: 'lin' } | |
输出:false |
# 本节介绍
本节是 “判断对象是否为空” 这道题的讲解,这道题看似非常简单,似乎一行代码就可以解决,但在真实的项目中还是不会直接在业务代码中判断,而是会封装成一个函数,本节将会介绍三种实现本题的方式。
# 知识点
- for in
- Object.keys()
- JSON.stringify()
# 题解
# 1.for in
首先可以用遍历解决,用的是 for in,思路如下:
- 遍历这个对象
- 如果能被遍历,说明这个对象有属性,返回
false
- 否则说明对象为空,返回
true
- 如果能被遍历,说明这个对象有属性,返回
代码实现如下:
function isEmptyObject(obj) { | |
for (let o in obj) { | |
return false; | |
} | |
return true; | |
} |
# 2.Object.keys()
还有一种解题方式就是使用 Object.keys(),先把对象转化为数组,然后再根据数组的长度是否为零来判断对象是否为空。
代码实现如下:
function isEmptyObject(obj) { | |
return Object.keys(obj).length === 0; | |
} |
虽然本题所有的测试用例都是引用类型,但是如果把本方法用到实际的项目中,传入的参数就是不可预知的了,很有可能就是一些奇怪的数据,比如传入 null
或 undefined
,就会出问题,如下代码所示:
function isEmptyObject(obj) { | |
return Object.keys(obj).length === 0; | |
} | |
isEmptyObject(null); // Uncaught TypeError: Cannot convert undefined or null to object | |
isEmptyObject(undefined); // Uncaught TypeError: Cannot convert undefined or null to object |
所以在实际项目中,可以用如下方式实现本函数:
function isEmptyObject(obj) { | |
return Object.keys(obj || []).length === 0; | |
} |
# 3.JSON.stringify()
还可以使用 JSON.stringify() 来解决本题,只需要判断对象转为 JSON
字符串之后,是不是等于 {}
即可,代码如下:
function isEmptyObject(obj) { | |
return JSON.stringify(obj) === "{}"; | |
} |
这么写判断大多数对象都不会有问题,不过一旦遇到出现 “循环引用” 的对象,就会报错,如下代码所示:
function isEmptyObject(obj) { | |
return JSON.stringify(obj) === "{}"; | |
} | |
// 循环引用该对象。 | |
const obj = { | |
a: 1, | |
}; | |
obj.a = obj; | |
isEmptyObject(obj); // 报错 |
所以在真实的项目中,这种写法也不是保险的,尽量不要用,虽然 bug 发生的概率特别小,但如果没有做容错处理,一旦发生了,就会阻断 js 代码执行,很有可能造成页面加载不出这种非常严重的 bug。
# 知识延伸
# 1. 如何判断原始数据为空
答:使用 !
运算符,把原始数据隐式转换为 Boolean
类型。
判断原始数据为空一般不需要封装成一个方法,直接写进业务代码即可,比如:
if (!target) { | |
// do something | |
} | |
while (!target) { | |
// do something | |
} |
# 2. 如何判断数组为空
答:直接判断数组长度为 0 即可,代码如下:
function isEmptyArray(arr) { | |
return Array.isArray(arr) && arr.length === 0; | |
} |
这种写法已经可以用到项目里了,不过,如果非要深入研究的话,JS 里的数组也是对象,所以数组下标可以是负数,也可以是小数,甚至是字符串都可以,只是这几种写法数组的 length
不会改变,代码如下:
const arr = []; | |
arr[-1] = "xxx"; | |
arr[0.5] = "xxx"; | |
arr["xxx"] = "xxx"; | |
console.log(arr.length); // 0 |
所以说,判断数组是否为空最完善的方法就是把数组当对象处理,直接用上面判断对象是否为空的方式判断即可。
const arr = []; | |
arr[-1] = "xxx"; | |
arr[0.5] = "xxx"; | |
arr["xxx"] = "xxx"; | |
function isEmptyObject(obj) { | |
for (let o in obj) { | |
return false; | |
} | |
return true; | |
} | |
console.log(isEmptyObject(arr)); // false |
不过上述都是很边界的情况,在平时开发时直接判断数组长度为 0 也没什么问题。
# 本节总结
本节我们学习了如何判断一个数据为空,最终的结论是:
- 判断对象为空,就使用
for in
或者Object.keys
来判断,尽量不要使用JSON.stringify
。 - 判断数组为空,一般判断长度为 0,也可以用和判断对象为空一样的方法来判断。
- 判断原始类型为空,就使用
!
运算符来判断。
学完本节后,同学们可以把 isEmptyObject
这个工具函数在项目里用起来了。
# 知识讲解:this
JavaScript 中的 this
对初学者来说一直是一个棘手的问题,因为它的指向实在太灵活了,如果没有真的理解它,在面试或笔试中被问到 this
指向谁,就只能靠猜。不仅是面试,实际开发中用到 this
的场景也特别多,很多隐蔽的 bug
都有可能是它导致的,用好了 this
,也能写出更简洁的代码。
本节实验我们一起来学一学 this
吧~
# 知识点
- this
- call、apply、bind
# this 是什么
很多同学老是记不住 this
指向谁,我觉得这是非常正常的现象,因为 this
指向的情况本身就比较多,面对这种不太好记的知识点,我们可以延续之前的学习方式,采用敲代码实验 + 画图记忆 + 真题训练的方式,这种学习方式能让大家学知识更高效一点。
首先,学习一个知识,先要理解它是什么,在大多数面向对象语言中, this
表示当前对象的一个引用,而 JS 中的 this
是完全不同的概念。
我们查阅 MDN 文档可知, this
是当前执行上下文( global
、 function
或 eval
)的一个属性。
也就是说,我们可以把 JS 中的 this
分为三种,分别是:
- 全局上下文中的
this
。 - 函数上下文中的
this
。 eval
上下文中的this
。
关于 eval ,在 MDN 或很多书籍和文档中都建议永远不要使用它,我们就不讨论它了,接下来,我们将主要讨论全局上下文和函数上下文的 this
。
有一点需要注意的是, node
环境中的 this
和 web
环境中的 this
是不同的,为了避免大家一开始接受太多概念,我们先只讨论 web
环境的 this
。
接下来,我们就通过做实验的方式,来学习一下 this
的指向。
# 全局上下文的 this
全局上下文的 this
指向非常明确, 不管有没有启用严格模式,都指向 window
对象 。
同学们可以打开右侧的实验环境,我们敲代码来验证一下。
随便新建一个 test.html
文件,在文件中写入如下代码:
<body> | |
<script> | |
console.log(this === window); // 输出 true | |
("use strict"); | |
console.log(this === window); // 输出 true | |
</script> | |
</body> |
然后可以启动一下环境中的 web server,查看代码输出情况,可以看到,的确是不管有没有启用严格模式,全局上下文的 this
都指向 window
对象。
给 this
添加属性,就相当于给 window
添加属性,给 window
添加属性,就相当于给 this
添加属性,如下代码所示:
this.userName = "zhangsan"; | |
window.age = 18; | |
console.log(this.age); // 输出 18 | |
console.log(window.userName); // 输出 'zhangsan' |
# 函数上下文的 this
全局上下文的 this
很简单,记住无脑指向 window
就完事,复杂就复杂在函数上下文的 this
,函数上下文中的 this
与 arguments
一样,就是函数的隐式参数,可以在任意函数中调用,它的指向不是固定不变的,取决于函数 处于什么位置、以什么方式调用 ,可以总结成如下图:
接下来,我们一起敲代码来验证上图的内容。
# 全局上下文中的函数
直接调用全局上下文中的函数, this
指向默认情况下为 window
。
function fn() { | |
console.log(this); // 输出 window | |
} | |
fn(); |
function fn() { | |
let a = 1; | |
console.log(this.a); // 输出 2 | |
} | |
var a = 2; | |
fn(); |
但是在严格模式下, this
指向的就是 undefined
。
function fn() { | |
"use strict"; | |
console.log(this); // 输出 undefined | |
} | |
fn(); |
# 对象中的函数
调用对象中的函数, this
指向为这个对象。
const obj = { | |
a: 1, | |
fn() { | |
console.log("this :>> ", this); // 输出 obj | |
console.log("this.a :>> ", this.a); // 输出 1 | |
}, | |
}; | |
obj.fn(); |
但是如果函数嵌套有函数,此时的 this
指向为 window
,就非常令人迷惑。
const obj = { | |
a: 1, | |
fn() { | |
return function () { | |
console.log("this :>> ", this); // 输出 window | |
console.log("this.a :>> ", this.a); // 输出 100 | |
}; | |
}, | |
}; | |
var a = 100; | |
obj.fn()(); |
其实可以这么理解:
obj.fn()(); | |
等价于; | |
const temp = obj.fn(); // 定义一个临时变量来存储 obj.fn 返回的函数 | |
temp(); // 执行这个函数 |
上面代码示例中的 temp
在运行时是处在 window
环境中的,所以 this
指向 window
。
# 箭头函数
遇到对象里有函数嵌套函数的情况,想要 this
指向这个对象,es6 之前,可以用一个临时变量 _this
来暂存 this
,代码如下:
const obj = { | |
a: 1, | |
fn() { | |
const _this = this; | |
return function () { | |
console.log("this :>> ", _this); // 输出 obj | |
console.log("this.a :>> ", _this.a); // 输出 1 | |
}; | |
}, | |
}; | |
obj.fn()(); |
es6 推出了箭头函数的概念,之后我们就可以使用箭头函数解决这个问题,代码如下:
const obj = { | |
a: 1, | |
fn() { | |
return () => { | |
console.log("this :>> ", this); // 输出 obj | |
console.log("this.a :>> ", this.a); // 输出 1 | |
}; | |
}, | |
}; | |
obj.fn()(); |
对于普通函数来说,内部的 this
指向函数运行时所在的对象。
对于箭头函数,它不会创建自己的 this
,它只会 从自己的作用域链的上一层继承 this 。
所以这里 fn
中嵌套的匿名箭头函数中的 this
,指向它作用域链的上一层的 this
,也就是函数 fn
的 this
,也就是 obj
。
其实,箭头函数内部实现也是定义临时变量 _this
来暂存 this
,用 babel
一试便知,如下图:
babel 在线地址
# 构造函数
构造函数内,如果返回值是一个对象, this
指向这个对象,否则 this
指向新建的实例。
function Person(name) { | |
this.name = name; | |
} | |
const p = new Person("zhangsan"); | |
console.log(p.name); // 'zhangsan' |
function Person(name) { | |
this.name = name; | |
return { | |
name: "xxx", | |
}; | |
} | |
const p = new Person("zhangsan"); | |
console.log(p.name); // 'xxx' |
function Person(name) { | |
this.name = name; | |
return {}; | |
} | |
const p = new Person("zhangsan"); | |
console.log(p.name); // 'undefined' |
如果有返回值,但是不是一个对象, this
还是指向实例。
function Person(name) { | |
this.name = name; | |
return 123; | |
} | |
const p = new Person("zhangsan"); | |
console.log(p.name); // 'zhangsan' |
这一点,在实现 new
操作符那一节,我们详细讲过。
const obj = { | |
a: 1, | |
fn() { | |
const _this = this; | |
return function () { | |
console.log("this :>> ", _this); // 输出 obj | |
console.log("this.a :>> ", _this.a); // 输出 1 | |
}; | |
}, | |
}; | |
obj.fn()(); // 输出 obj 和 1 | |
console.log("========"); | |
let temp = obj.fn(); | |
var a = 2; // 没用 | |
temp(); // 输出 obj 和 1 | |
console.log("========"); | |
obj.a = 3; | |
temp(); // 输出 obj 和 3 | |
// 这里用到的是函数闭包 |
const obj = { | |
a: 1, | |
fn() { | |
return () => { | |
console.log("this :>> ", this); // 输出 obj | |
console.log("this.a :>> ", this.a); // 输出 1 | |
}; | |
}, | |
}; | |
// obj.fn()(); | |
obj.fn()(); // 输出 obj 和 1 | |
console.log("========"); | |
// let temp = obj.fn(); | |
let fn_global = obj.fn; | |
let temp = fn_global(); | |
var a = 2; // 有用 | |
temp(); // 输出 obj 和 2 | |
console.log("========"); | |
obj.a = 3; | |
temp(); // 输出 obj 和 2 | |
// 这里用到的是函数闭包 |
# 显式改变函数上下文的 this
了解了函数中的 this
指向后,我们可以使用 call
、 apply
和 bind
来显式改变函数中的 this
指向。
# call
Function.prototype.call() 方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
function fn() { | |
console.log(this.name); | |
} | |
const obj = { | |
name: "zhangsan", | |
}; | |
fn.call(obj); // 指定 this 为 obj,输出 'zhangsan' |
# apply
Function.prototype.apply() 方法调用一个具有给定 this
值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
apply
和 call
的功能完全一样,只是传参形式不一样, call
是传多个参数, apply
是只传参数集合。
function add(x, y, z) { | |
return this.x + this.y + this.z; | |
} | |
const obj = { | |
x: 1, | |
y: 2, | |
z: 3, | |
}; | |
console.log(add.call(obj, 1, 2, 3)); // 输出 6 | |
console.log(add.apply(obj, [1, 2, 3])); // 输出 6,只是传参形式不同而已 |
# bind
Function.prototype.bind() 方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
bind
和 call
、 apply
的区别是,函数调用 call
和 apply
会直接调用,而调用 bind
是创建一个新的函数,必须手动去再调用一次,才会生效。
function add(x, y, z) { | |
return this.x + this.y + this.z; | |
} | |
const obj = { | |
x: 1, | |
y: 2, | |
z: 3, | |
}; | |
const add1 = add.bind(obj, 1, 2, 3); //bind 会返回一个新的函数 | |
console.log(add1()); // 执行新的函数,输出 6 |
# 小测验
在之前的实验中,我们已经把 this
所有的输出情况都手敲了一遍,接下来,我们通过一些练习题,巩固一下之前学的知识。
# 第 1 题
function fn() { | |
console.log(this); // ? | |
} | |
fn(); |
输出:
window; |
原因:全局上下文中的函数,非严格模式指向 window
。
# 第 2 题
function fn() { | |
"use strict"; | |
console.log(this); // ? | |
} | |
fn(); |
输出:
undefined; |
原因:全局上下文中的函数,严格模式指向 undefined
。
# 第 3 题
const obj = { | |
userName: "zhangsan", | |
fn() { | |
console.log(this); // ? | |
console.log(this.userName); // ? | |
}, | |
}; | |
obj.fn(); |
出:
{ userName: 'zhangsan', fn: ƒ} | |
'zhangsan' |
原因:对象中的函数, this
指向这个对象。
# 第 4 题
const obj = { | |
userName: "zhangsan", | |
fn() { | |
console.log(this); // ? | |
console.log(this.userName); // ? | |
}, | |
}; | |
const fn = obj.fn; | |
fn(); |
输出:
window; | |
undefined; |
原因:函数 fn
是处在 window
环境下的,所以 this
指向为 window
, window
上没有 userName
这个字段,所以为 undefined
。
# 第 5 题
const person = { | |
userName: "zhangsan", | |
wife: { | |
userName: "xxx", | |
fn() { | |
console.log(this); // ? | |
console.log(this.userName); // ? | |
}, | |
}, | |
}; | |
person.wife.fn(); |
出:
{userName: 'xxx', fn: ƒ} | |
'xxx' |
原因:函数 fn
调用的时候是 wife.fn()
,所以 this
指向为 wife
对象。
# 第 6 题
const o1 = { | |
text: "o1", | |
fn() { | |
return this.text; | |
}, | |
}; | |
const o2 = { | |
text: "o2", | |
fn() { | |
return o1.fn(); | |
}, | |
}; | |
const o3 = { | |
text: "o3", | |
fn() { | |
var fn = o1.fn; | |
return fn(); | |
}, | |
}; | |
console.log(o1.fn()); // ? | |
console.log(o2.fn()); // ? | |
console.log(o3.fn()); // ? |
输出:
"o1"; | |
"o1"; | |
undefined; |
原因: o1.fn()
中的 this
指向 o1
对象,所以输出的 this.text
为 'o1'
。
执行 o2.fn()
等价于执行 o1.fn()
,所以输出 'o1'
。
o3.fn()
理解起来稍微复杂一点, o3
对象中的代码等价于下面的代码:
const o3 = { | |
text: "o3", | |
fn() { | |
return function () { | |
return this.text; | |
}; | |
}, | |
}; | |
console.log(o3.fn()()); |
所以 this
指向为 window
, window.text
为 undefined
。
# 第 7 题
const o1 = { | |
text: "o1", | |
fn() { | |
return this.text; | |
}, | |
}; | |
const o2 = { | |
text: "o2", | |
fn: o1.fn, | |
}; | |
console.log(o1.fn()); // ? | |
console.log(o2.fn()); // ? |
输出:
"o1"; | |
"o2"; |
原因: o1.fn()
中的 this
指向 o1
对象,所以输出的 this.text
为 'o1'
。
o2
对象中的代码等价于下面的代码:
const o2 = { | |
text: "o2", | |
fn() { | |
return this.text; | |
}, | |
}; |
所以 this
指向为 o2
对象,所以输出的 this.text
为 'o2'
。
# 第 8 题
const foo = { | |
name: "zhangsan", | |
sayName() { | |
console.log(this.name); | |
}, | |
}; | |
const bar = { | |
name: "xxx", | |
}; | |
foo.sayName.call(bar); // ? | |
foo.sayName.apply(bar); // ? | |
foo.sayName.bind(bar)(); // ? |
输出:
"xxx"; | |
"xxx"; | |
"xxx"; |
原因:使用 call
、 apply
和 bind
都显式改变了 this
的指向,所以都指向 bar
对象。
# 第 9 题
function Person1(name) { | |
this.name = name; | |
} | |
function Person2(name) { | |
this.name = name; | |
return {}; | |
} | |
function Person3(name) { | |
this.name = name; | |
return { | |
name: "xxx", | |
}; | |
} | |
function Person4(name) { | |
this.name = name; | |
return 1; | |
} | |
const p1 = new Person1("zhangsan"); | |
const p2 = new Person2("zhangsan"); | |
const p3 = new Person3("zhangsan"); | |
const p4 = new Person4("zhangsan"); | |
console.log(p1.name); // ? | |
console.log(p2.name); // ? | |
console.log(p3.name); // ? | |
console.log(p4.name); // ? |
输出:
"zhangsan"; | |
undefined; | |
"xxx"; | |
"zhangsan"; |
原因:构造函数内,如果返回值是一个对象, this
指向这个对象,否则 this
指向新建的实例。
# 第 10 题
var userName = "xxx"; | |
const obj1 = { | |
userName: "zhangsan", | |
fn() { | |
setTimeout(function () { | |
console.log(this.userName); // ? | |
}); | |
}, | |
}; | |
const obj2 = { | |
userName: "zhangsan", | |
fn() { | |
setTimeout(() => { | |
console.log(this.userName); // ? | |
}); | |
}, | |
}; | |
obj1.fn(); | |
obj2.fn(); |
输出:
"xxx"; | |
"zhangsan"; |
原因: obj1
中调用的 this
是函数嵌套函数中的 this
,所以指向 window
,输出 window.userName
为 'xxx'
。
obj2
中因为箭头函数的原因, this
指向上层作用域,所以指向对象 obj2
,输出 obj2.userName
为 'zhangsan'
。
# 第 11 题
var userName = "xxx"; | |
const obj1 = { | |
userName: "lin", | |
fn() { | |
return () => { | |
console.log(this.userName); // ? | |
}; | |
}, | |
}; | |
const obj2 = { | |
userName: "lin", | |
fn() { | |
return () => { | |
return () => { | |
console.log(this.userName); // ? | |
}; | |
}; | |
}, | |
}; | |
obj1.fn()(); | |
obj2.fn()()(); |
出:
"lin"; | |
"lin"; |
原因:因为有箭头函数,嵌套多少层,最终都会一层一层地读到第一层,所以 this
指向为对象本身。
# 第 12 题
function foo() { | |
return () => { | |
console.log(this.a); | |
}; | |
} | |
const obj1 = { | |
a: 2, | |
}; | |
const obj2 = { | |
a: 3, | |
}; | |
const bar = foo.call(obj1); // ? | |
bar.call(obj2); // ? |
输出:
2; |
原因: foo.call(obj1)
输出 2 是因为 call
显式改变 this
指向。
bar.call(obj2)
啥也不输出,因为箭头函数的绑定无法被修改
# 第 13 题
var a = 100; | |
const foo = () => () => { | |
console.log(this.a); | |
}; | |
const obj1 = { | |
a: 2, | |
}; | |
const obj2 = { | |
a: 3, | |
}; | |
const bar = foo.call(obj1); // ? | |
bar.call(obj2); // ? |
输出:
100; |
原因同第 12 题。
# 一些参考资料
const o1 = { | |
text: "o1", | |
fn() { | |
console.log(this); | |
return this.text; | |
}, | |
}; | |
const o2 = { | |
text: "o2", | |
fn() { | |
return o1.fn(); | |
}, | |
}; | |
const o3 = { | |
text: "o3", | |
fn() { | |
var fn = o1.fn; | |
// console.log(this.text); | |
let res = fn(); | |
return res; | |
// return fn(); | |
}, | |
}; | |
console.log(o1.fn()); // ? | |
console.log(o2.fn()); // ? | |
console.log(o3.fn()); // ? undefined |
JavaScript 中的 this 到底是什么? - 掘金 (juejin.cn)
彻底搞懂 JavaScript 中的 this 指向问题 - 西岭老湿的文章 - 知乎
https://zhuanlan.zhihu.com/p/42145138
const o1 = { | |
text: "o1", | |
fn() { | |
console.log(this); | |
return this.text; | |
}, | |
}; | |
const o2 = { | |
text: "o2", | |
fn() { | |
return o1.fn(); | |
}, | |
}; | |
const o3 = { | |
text: "o3", | |
fn() { | |
var fn = o1.fn; | |
// console.log(this.text); | |
let res = fn(); | |
return res; | |
// return fn(); | |
}, | |
}; | |
console.log(o1.fn()); // ? | |
console.log(o2.fn()); // ? | |
console.log(o3.fn()); // ? undefined |
# 实现 call 函数
# 挑战介绍
本节我们来挑战一道大厂面试真题 —— 实现 call
函数。
# 挑战准备
新建一个 myCall.js
文件,在文件里写一个名为 myCall
的函数,并导出这个函数,如下图所示:
这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,函数代码如下:
function myCall(fn, context) { | |
// 补充代码 | |
} | |
module.exports = myCall; |
# 挑战内容
请封装一个 myCall
函数,用来实现 call
函数的功能。
myCall
函数接收多个参数,第一个参数是 fn
,代表要执行的函数,第二个参数是 context
,代表需要显式改变的 this
指向,之后的参数都是 fn
的参数。
示例:
const person = { | |
userName: "zhangsan", | |
}; | |
function fn() { | |
return this.userName; | |
} | |
myCall(fn, person); // 执行函数 fn,返回 'zhangsan' |
//fn 传参数的情况 | |
const obj = { | |
count: 10, | |
}; | |
function fn(x, y, z) { | |
return this.count + x + y + z; | |
} | |
myCall(fn, obj, 1, 2, 3); // 执行函数 fn,返回 16 |
暂时觉得这样做不行,这里是将函数设置为对象的属性的,假如对象本来有这个属性名,这样会不会覆盖了对象原来的属性值,
可以改写成继续使用函数的 call 的么?
# 本节介绍
本节是 “实现 call
函数” 这道题的讲解,在面试场景中,光是能说清楚 this
的指向是不够的,面试官很喜欢考查 call
、 apply
和 bind
这三个函数的用法和原理,本节我们就先从 call
这个函数开始学起,在讲解 call
函数的原理的同时,也会介绍一些它的常用使用场景。
# 知识点
- call 原理
- call 使用场景
# 题解
在讲解之前,先给出本题的答案,代码如下:
function myCall(fn, context = window) { | |
context.fn = fn; | |
const args = [...arguments].slice(2); | |
const res = context.fn(...args); | |
delete context.fn; | |
return res; | |
} |
# 题解分析
在尝试去实现 call
函数之前,我们再来回顾一下它的用法, call
函数是为了改变 this
的指向,代码如下:
var userName = "xxx"; | |
const person = { | |
userName: "zhangsan", | |
}; | |
function fn() { | |
console.log(this.userName); | |
} | |
fn.call(); // 直接调用,this 指向 window,输出 'xxx' | |
fn.call(person); // 用 call,this 指向 person,输出 'zhangsan' |
call
是写到 Function.prototype
上的方法,而本节我们要实现的 myCall
是把函数当作参数传递进去,两者只是调用形式不同,原理都是一样的。
我们尝试来实现一下显式改变 this
指向的功能,调用对象中的函数, this
指向为这个对象,所以我们需要做的操作是:
- 把函数
fn
挂载到要指向的对象context
上。 - 执行
context.fn
,执行完了删除context
上的fn
函数,避免对传入对象的属性造成污染。
代码实现如下:
function myCall(fn, context) { | |
// 把函数 fn 挂载到对象 context 上。 | |
context.fn = fn; | |
// 执行 context.fn | |
context.fn(); | |
// 执行完了删除 context 上的 fn 函数,避免对传入对象的属性造成污染。 | |
delete context.fn; | |
} |
测试一下:
var userName = "xxx"; | |
const person = { | |
userName: "zhangsan", | |
}; | |
function fn() { | |
return this.userName; | |
} | |
myCall(fn, person); // 输出 'zhangsan' | |
myCall(fn, window); // 输出 'xxx' |
可以看到,仅仅三行代码,我们就实现了 call
函数的核心功能。
不过这里面有一些其他细节需要处理,比如:
- 要处理
context
不传值的情况,传一个默认值window
。 - 处理函数
fn
的参数,执行fn
函数时把参数携带进去。 - 获取执行函数
fn
产生的返回值,最终返回这个返回值。
最终实现代码如下:
// 要处理 context 不传值的情况,传一个默认值 window。 | |
function myCall(fn, context = window) { | |
context.fn = fn; | |
// 处理函数 fn 的参数,执行 fn 函数时把参数携带进去。 | |
const args = [...arguments].slice(2); | |
// 获取执行函数 fn 产生的返回值。 | |
const res = context.fn(...args); | |
delete context.fn; | |
// 最终返回这个返回值 | |
return res; | |
} |
测试一下:
const obj = { | |
count: 10, | |
}; | |
function fn(x, y, z) { | |
console.log(this.count + x + y + z); | |
} | |
myCall(fn, obj, 1, 2, 3); // 执行函数 fn,输出 16 |
这样我们就实现了 call
函数该有的功能,原生的 call
函数是写到 Function.prototype
上的方法,我们也尝试在函数的原型上实现一个 myCall
函数,只需稍加改造即可,代码实现如下:
// 写到函数的原型上,就不需要把要执行的函数当作参数传递进去 | |
Function.prototype.myCall = function (context = window) { | |
// 这里的 this 就是这个要执行的函数 | |
context.fn = this; | |
// 参数少了一个,slice (2) 改为 slice (1) | |
const args = [...arguments].slice(1); | |
const res = context.fn(...args); | |
delete context.fn; | |
return res; | |
}; |
测试一下:
const obj = { | |
count: 10, | |
}; | |
function fn(x, y, z) { | |
console.log(this.count + x + y + z); | |
} | |
fn.myCall(obj, 1, 2, 3); // 执行函数 fn,输出 16 |
# 知识延伸
# 处理边缘情况
上文在函数原型上实现的 myCall
函数,还有优化的空间,有一些边缘的情况,可能会导致报错,比如把要指向的对象指向一个原始值,代码如下:
fn.myCall(0); // Uncaught TypeError: context.fn is not a function |
这时,就需要参考一下原生的 call
函数是如何解决的这个问题,我们打印出来看一下:
var userName = "xxx"; | |
const person = { | |
userName: "zhangsan", | |
}; | |
function fn(type) { | |
console.log(type, "->", this.userName); | |
} | |
fn.call(0, "number"); | |
fn.call(1n, "bigint"); | |
fn.call(false, "boolean"); | |
fn.call("123", "string"); | |
fn.call(undefined, "undefined"); | |
fn.call(null, "null"); | |
const a = Symbol("a"); | |
fn.call(a, "symbol"); | |
fn.call([], "引用类型"); |
可以看到, undefined
和 null
指向了 window
,原始类型和引用类型都是 undefined
。
其实是因为,原始类型指向对应的包装类型,引用类型就指向这个引用类型,之所以输出值都是 undefined
,是因为这些对象上都没有 userName
属性。
改造一下我们的 myCall
函数,实现原始类型的兼容,代码如下:
Function.prototype.myCall = function (context = window) { | |
if (context === null || context === undefined) { | |
context = window; //undefined 和 null 指向 window | |
} else { | |
context = Object(context); // 原始类型就包装一下 | |
} | |
context.fn = this; | |
const args = [...arguments].slice(1); | |
const res = context.fn(...args); | |
delete context.fn; | |
return res; | |
}; |
还有另外一种边缘情况,假设对象上本来就有一个 fn
属性,执行下面的调用,对象上的 fn
属性会被删除,代码如下:
const person = { | |
userName: "zhangsan", | |
fn: 123, | |
}; | |
function fn() { | |
console.log(this.userName); | |
} | |
fn.myCall(person); | |
console.log(person.fn); // 输出 undefined,本来应该输出 123 |
因为对象上本来的 fn
属性和 myCall
函数内部临时定义的 fn
属性重名了。
还记得 Symbol
的作用吗,可以用 Symbol
来防止对象属性名冲突问题,继续改造 myCall
函数,代码实现如下:
Function.prototype.myCall = function (context = window) { | |
if (context === null || context === undefined) { | |
context = window; | |
} else { | |
context = Object(context); | |
} | |
const fn = Symbol("fn"); // 用 symbol 处理一下 | |
context[fn] = this; | |
const args = [...arguments].slice(1); | |
const res = context[fn](...args); | |
delete context[fn]; | |
return res; | |
}; |
至此,一个功能尽可能完善的 myCall
函数,终于写完了。
let a1 = Symbol("a"); | |
let a2 = Symbol("a"); | |
console.log(a1); | |
console.log(a2); | |
let obj = {}; | |
obj[a1] = "hello"; | |
console.log(obj[a1]); // "hello" | |
console.log(obj[a2]); // undefined |
# call 使用场景
call
的使用场景非常多,所有调用 call
的使用场景都是为了显式地改变 this
的指向,能用 call
解决的问题也能用 apply
解决,因为它们俩只是传参形式不同。下面一起来看 call
常用的四个使用场景。
# 1. 精准判断一个数据类型
精准地判断一个数据的类型,可以用到 Object.prototype.toString.call(xxx)
。
调用该方法,统一返回格式 [object Xxx]
的字符串,用来表示该对象。
// 引用类型 | |
console.log(Object.prototype.toString.call({})); // '[object Object]' | |
console.log(Object.prototype.toString.call(function () {})); // "[object Function]' | |
console.log(Object.prototype.toString.call(/123/g)); // '[object RegExp]' | |
console.log(Object.prototype.toString.call(new Date())); // '[object Date]' | |
console.log(Object.prototype.toString.call(new Error())); // '[object Error]' | |
console.log(Object.prototype.toString.call([])); // '[object Array]' | |
console.log(Object.prototype.toString.call(new Map())); // '[object Map]' | |
console.log(Object.prototype.toString.call(new Set())); // '[object Set]' | |
console.log(Object.prototype.toString.call(new WeakMap())); // '[object WeakMap]' | |
console.log(Object.prototype.toString.call(new WeakSet())); // '[object WeakSet]' | |
// 原始类型 | |
console.log(Object.prototype.toString.call(1)); // '[object Number]' | |
console.log(Object.prototype.toString.call("abc")); // '[object String]' | |
console.log(Object.prototype.toString.call(true)); // '[object Boolean]' | |
console.log(Object.prototype.toString.call(1n)); // '[object BigInt]' | |
console.log(Object.prototype.toString.call(null)); // '[object Null]' | |
console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]' | |
console.log(Object.prototype.toString.call(Symbol("a"))); // '[object Symbol]' |
这里需要调用 call
就是为了显式地改变 this
指向为我们的目标变量。
如果不改变 this
指向为我们的目标变量 xxx
, this
将永远指向调用的 Object.prototype
,也就是原型对象,对原型对象调用 toString
方法,结果永远都是 [object Object]
,如下代码所示:
Object.prototype.toString([]); // 输出 '[object Object]' 不调用 call,this 指向 Object.prototype,判断类型为 Object。 | |
Object.prototype.toString.call([]); // 输出 '[object Array]' 调用 call,this 指向 [],判断类型为 Array | |
Object.prototype.toString(1); // 输出 '[object Object]' 不调用 call,this 指向 Object.prototype,判断类型为 Object。 | |
Object.prototype.toString.call(1); // 输出 '[object Number]' 调用 call,this 指向包装对象 Number {1},判断类型为 Number |
# 2. 伪数组转数组
伪数组转数组,在 es6 之前,可以使用 Array.prototype.slice.call(xxx)
。
function add() { | |
const args = Array.prototype.slice.call(arguments); | |
// 也可以这么写 const args = [].slice.call (arguments) | |
args.push(1); // 可以使用数组上的方法了 | |
} | |
add(1, 2, 3); |
原理同精准判断一个数据类型相同,如果不改变 this
指向为目标伪数组, this
将永远指向调用的 Array.prototype
,就不会生效。
// 从 slice 方法原理理解为什么要调用 call | |
Array.prototype.slice = function (start, end) { | |
const res = []; | |
start = start || 0; | |
end = end || this.length; | |
for (let i = start; i < end; i++) { | |
res.push(this[i]); // 这里的 this 就是伪数组,所以要调用 call | |
} | |
return res; | |
}; |
# 3.ES5 实现继承
在一个子构造函数中,你可以通过调用父构造函数的 call
方法来实现继承。
function Person(name) { | |
this.name = name; | |
} | |
function Student(name, grade) { | |
Person.call(this, name); | |
this.grade = grade; | |
} | |
const p1 = new Person("zhangsan"); | |
const s1 = new Student("zhangsan", 100); |
上面的代码示例中,构造函数 Student
中会拥有构造函数 Person
中的 name
属性, grade
属性是 Student
自己的。
这里的代码如果换成 ES6 的,就等价于下面的代码:
class Person { | |
constructor(name) { | |
this.name = name; | |
} | |
} | |
class Student extends Person { | |
constructor(name, grade) { | |
super(name); | |
this.grade = grade; | |
} | |
} | |
const p1 = new Person("zhangsan"); | |
const s1 = new Student("zhangsan", 100); |
关于继承,同学们掌握 ES6 的实现方式就好,ES5 的做了解即可,因为现在大家基本上都用 ES6 的写法了,如果想对 ES5 的继承有深入研究,可以去看一下《JavaScript 高级程序设计(第 4 版)》原型和原型链相关的章节。
# 4. 处理回调函数 this 丢失问题
执行下面的代码,回调函数会导致 this
丢失。
const obj = { | |
userName: "zhangsan", | |
sayName() { | |
console.log(this.userName); | |
}, | |
}; | |
obj.sayName(); // 输出 'zhangsan' | |
function fn(callback) { | |
if (typeof callback === "function") { | |
callback(); | |
} | |
} | |
fn(obj.sayName); // 输出 undefined |
导致这样现象的原因是回调函数执行的时候 this
指向已经是 window
了,所以输出 undefined
。
可以使用 call
改变 this
指向,代码如下:
const obj = { | |
userName: "zhangsan", | |
sayName() { | |
console.log(this.userName); | |
}, | |
}; | |
obj.sayName(); // 输出 'zhangsan' | |
function fn(callback, context) { | |
// 定义一个 context 参数,可以把上下文传进去 | |
if (typeof callback === "function") { | |
callback.call(context); // 显式改变 this 值,指向传入的 context | |
} | |
} | |
fn(obj.sayName, obj); // 输出 'zhangsan' |
# 本节总结
本节我们介绍了 call
函数的实现原理,最关键的点就是要显式地改变 this
的指向,其余全部都是在处理一些边界情况。
在知识延伸部分,我们介绍了 call
函数一些常用的使用场景,所有调用 call
的使用场景还是为了显式地改变 this
的指向。
需要注意的是,能用 call
解决的问题也能用 apply
解决,因为它们俩只是传参形式不同。