1. 如何正确判断this
的指向
如果用一句话说明this
的指向,那么就是:谁调用它,this
就指向谁。
按照以下顺序准确判断this
的指向:
1. 全局环境中的this
- 浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)
this
都指向全局对象window
; node
环境:无论是否在严格模式下,在全局执行环境中(在任何函体外部),this
都是空对象{};
2. 是否是new
绑定
- 如果是
new
绑定,并且构造函数中没有返回function
或者object
,那么this
指向这个新对象。
如下:
构造函数返回值不是
function
或object
function Super(age) {
this.age = age;
}
let instance = new Super('26');
console.log(instance.age); //26
复制代码
构造函数返回值是
function
或object
,这种情况下this
指向的是返回的对象
function Super(age) {
this.age = age;
let obj = { a: 2 };
return obj;
}
let instance = new Super('26');
console.log(instance.age); //undefined
复制代码
你可能想知道为什么会这样?我们看下new
的实现原理:
- 创建一个新对象
- 这个新对象会被执行原型方法
- 属性和方法会被加入到
this
引用的对象中,并执行构造函数中的方法 - 如果函数没有返回其他对象,俺么
this
指向这个新对象,否则this
指向构造函数中返回的对象
//new是关键字 这里仅是说明new的过程
function new(func){
let target = {};
target.__proto__ = func.prototype;
let res = func.call(target);
//排除null的情况
if(res && typeof(res)=='object' || typeof(res)=='function'){
return res;
}
return target;
}
复制代码
3. 函数是否通过call
,apply
调用,或者使用了bind
绑定,如果是,那么this
绑定的就是指定的对象【显示绑定】
function info() {
console.log(this.age);
}
let person = {
age: 20,
info
}
let age = 28;
let myInfo = person.info;
myInfo.call(person); //20
myInfo.apply(person); //20
myInfo.bind(person)(); //20
复制代码
这里需要注意一种特殊情况,如果call
,apply
,或者bind
传入的第一个参数值是undefined
或者null
,严格模式下this
的值为传入的值null
/undefined
。非严格模式下,实际应用的默认绑定规则,this
指向全局对象(node
环境为global
,浏览器环境为window
)
function info() {
//node环境中:非严格模式 global,严格模式为null
//浏览器环境中:非严格模式window,严格模式为null
console.log(this);
console.log(this.age);
}
var person = {
age: 20,
info
}
var age = 28; //注意如果使用let声明结果?
var myInfo = person.info;
//严格模式抛出错误
//非严格模式,node输出为undefined(因为全局的age不会挂在global上)
//非严格模式,浏览器环境输出28(因为全局的age会挂在window上)
myInfo.call(null);
复制代码
这里需要注意如果是用
let
声明,非严格模式喜爱浏览器输出undefined
现代 JavaScript 的变量作用域
4. 隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象,典型的隐式调用为:xxx.fn()
function info() {
console.log(this.age);
}
let person = {
age: 20,
info
}
let age = 28;
person.info(); //20,执行的是隐式绑定
复制代码
5. 默认绑定,在不能应用其他绑定规则时使用的默认规则,通尝试独立函数调用
非严格模式:node
环境执行全局对象global
,浏览器环境,执行全局对象window
严格模式:执行undefined
function info() {
console.log(this.age);
}
var age = 28;
//严格模式 抛错
//非严格模式 node下输出undefined(因为全局的age不会挂在global上)
//非严格模式 浏览器环境下输出28(因为全局的age不会挂在window上)
//严格模式抛错,因为this此时是undefined
info(); //28
复制代码
注意如果
age
是通过let
声明,输出是undefined
,因为通过let
声明的不会挂到window
对象上
6. 箭头函数的情况
箭头函数没有自己的this
,继承外层上下文绑定的this
let obj = {
age: 20,
info: function () {
return () => {
console.log(this.age); //this继承的是外层上下文绑定的this
}
}
}
let person = { age: 28 };
let info = obj.info();
info(); //20
let info2 = obj.info.call(person);
info2(); //28
复制代码
2. JS
中原始类型有哪几种?null
是对象吗?原始数据类型和复杂数据类型有什么区别?
目前,JS原始类型有6种,分别为:
Boolean
String
Number
Undefined
Null
Symbol
(ES6新增)
复杂数据类型只有1种:Object
null
不是对象,尽管typeof null
输出的是object
,这是一个历史遗留问题,Js的最初版本中使用的是32位系统,为了性能考虑使用低位存储变量的类型信息,000开头代表是对象,null
表示为全零,所以将它错误的判断为object
基本数据类型和复杂数据类型的区别:
1. 内存的分配不同
- 基本数据类型存储在栈中
- 复杂数据类型存储在堆中,栈中存储的变量,是指向堆中的引用地址
2. 访问机制不同
- 基本数据类型是按值访问
- 复杂数据类型按引用访问,Js不允许直接访问在堆内存中的对象,在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值
3.复制变量时不同
- 基本数据类型:
a=b;
是将b
中保存的原始值的副本赋值给新变量a
,a
和b
完全独立,互补影响 - 复杂数据类型:
a=b
;将b
保存的对象内存的引用地址赋值给新变量a
,a
和b
指向了同一个堆内存地址,其中一个值发生了改变,另一个也会改变。
let b = {
age: 10
}
let a = b;
a.age = 20;
console.log(b); //{age:20}
复制代码
4. 参数传递的不同(实参/形参)
函数传参都是按值传递(栈中存储的内容):基本数据类型,拷贝的是值,复杂数据类型,拷贝的是引用地址
//基本数据类型
let b = 10;
function change(info) {
info = 20;
}
//info=b;基本数据类型,拷贝的是值;复杂数据类型,拷贝的是引用地址
change(b);
console.log(b); //10
复制代码
//复杂数据类型
let b = {
age: 10
}
function change(info) {
info.age = 20;
}
//info=b;拷贝的是地址的引用,修改互相影响
change(b)
console.log(b); //{age:20}
复制代码
3. 对HTML5
语义化的理解
HTML5
语义化指的是合理正确的使用语义化的标签来创建页面结构,如header
,footer
,nav
,从标签上即可以直观的知道这个标签的作用,而不是滥用div
语义化标签主要有:
title
:主要用于页面的头部信息介绍header
:定义文档的页眉nav
:主要用于页面导航main
: 规定文档的主要内容,对于文档来说应该是唯一的,它不应包含在文档中重复出现的内容,比如侧栏、导航栏、版权信息、站点标志或搜索表单 参考article
:独立的自包含内容h1~h6
:定义标题ul
:定义无序列表ol
:定义有序列表address
:定义文档或文章的作者/拥有者的联系信息canvas
:绘制图像dialog
:定义一个对话框、确认框或者窗口aside
:定义其所处内容之外的内容。<aside>
的内容可用作文章的侧栏section
:定义文档中的节(section
、区段),比如章节、页眉、页脚或文档中的其他部分figure
:规定独立的流内容(图像、图表、照片、代码等等)。figure
元素的内容应该与主内容相关,但如果被删除,则不应该对文档流产生影响details
:描述文档或者文档某一部分细节mark
:带有记号的文本
4. 如何让( a==1 && a==2 && a==3 )
的值为true
1. 利用隐式转换规则
==
操作符在左右数据类型不一致时,会先进行隐式转换a==1 && a==2 && a==3
的值意味着不可能时基本数据类型,如果a
是null
或者是undefined
bool
类型,都不可能返回true
- 因此可以推测
a
是复杂数据类型,Js
中复杂数据类型只有object
,回忆下Object
转换为原始类型会调用什么方法? 参考- 如果部署了
[Symbol.toPrimitive]
接口,那么调用此接口,若返回的不是基本数据类型,抛出错误 - 如果没有部署
[Symbol.toPrimitive]
接口,那么根据需要转换的类型,先调用valueOf
/toString
- JavaScript 对象转换到基本类型值算法 ToPrimitive
- 如果部署了
let obj = {
[Symbol.toPrimitive](hint) {
console.log(hint);
return 10;
},
valueOf() {
console.log('valueOf');
return 20;
},
toString() {
console.log('toString');
return 'hello';
}
}
console.log(obj + 'tte'); //default
//如果没有部署[Symbol.toPrimitive]接口,调用顺序为valueOf -> toString
console.log(obj == 'tte'); //defalut
//如果没有部署[Symbol.toPrimitive]接口,调用顺序为valueOf -> toString
console.log(obj * 10); //number
//如果没有部署[Symbol.toPrimitive]接口,调用顺序为valueOf -> toString
console.log(Number(obj)); //number
//如果没有部署[Symbol.toPrimitive]接口,调用顺序为valueOf -> toString
console.log(String(obj)); //string
//如果没有部署[Symbol.toPrimitive]接口,调用顺序为toString -> valueOf
复制代码
那么对于这道题,只要[Symbol.toPrimitive]
接口第一次返回的值是1,然后递增,即成立。
let a = {
[Symbol.toPrimitive]: (function (hint) {
let i = 1;
return function () {
return i++;
}
})()
}
console.log(a == 1 && a == 2 && a == 3);
复制代码
调用valueOf
接口的情况:
let a = {
valueOf: (function () {
let i = 1;
return function () {
return i++;
}
})()
}
console.log(a == 1 && a == 2 && a == 3);
复制代码
调用
toString
接口与调用valueOf
类似
另外,除了i自增的方法外,还可以利用正则,如下:
let a = {
reg: /\d/g,
valueOf() {
return this.reg.exec(123)[0];
}
}
console.log(a == 1 && a == 2 && a == 3);
复制代码
2. 利用数据劫持
使用Object.defineProperty
定义的属性,在获取属性时,会调用get
方法,利用这个特性我们在window
对象上定义a
属性,如下:
let i = 1;
Object.defineProperty(window, 'a', {
get() {
return i++;
}
})
console.log(a == 1 && a == 2 && a == 3);
复制代码
ES6
新增了Proxy
,此处可以利用Proxy
去实现,如下:
let a = new Proxy({}, {
i: 1,
get() {
return () => this.i++
}
})
console.log(a == 1 && a == 2 && a == 3);
复制代码
3. 数组的toString接口默认调用数组的join方法,重写数组的join方法
let a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3);
复制代码
4. 利用with关键字
let i = 0;
with ({
get a() {
return ++i;
}
}) {
console.log(a == 1 && a == 2 && a == 3);
}
复制代码
5. 防抖(debounce)函数的左右是什么?有哪些应用场景,实现简单的防抖函数
防抖函数的作用:
控制函数在一定时间内的执行次数,防抖意味着N秒内函数只会被执行一次,如果N秒内再次被触发,则重新计算延迟时间。
防抖应用场景:
- 搜索框输入查询,如果用户一直在输入中,没有必要不停的调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力
- 表单验证
- 按钮提交事件
- 浏览器窗口缩放,
resize
事件等
防抖函数实现:
/**
*
* @param {*} func 要执行的函数
* @param {*} wait 延迟时间 毫秒
* @param {*} immediate true 表示开始会立即触发一次 false 表示最后一次一定会触发
*/
function debounce(func, wait, immediate = true) {
let timeout, context, args;
//延迟执行函数
const later = () => setTimeout(() => {
//延迟函数执行完毕,清空定时器
timeout = null;
//延迟执行的情况下,函数会在延迟函数中执行
//使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args);
context = args = null;
}
}, wait);
let debounced = (...params) => {
if (!timeout) {
timeout = later();
if (immediate) {
//立即执行
func.apply(this, params);
} else {
//闭包
context = this;
args = params;
}
} else {
//连续点击从头开始计算延迟时间
clearTimeout(timeout);
timeout = later();
}
}
debounced.cancel = () => {
clearTimeout(timeout);
timeout = null;
}
return debounced;
}
复制代码