# js基础面试题一

# 1、null和undefined的区别?

null 表示一个"无"的对象,也就是该处不应该有值;而 undefined 表示未定义。 在转换为数字时结果不同,Number(null) 为 0,而 undefined 为 NaN。

使用场景上:

  • ull:

    • 作为函数的参数,表示该函数的参数不是对象
    • 作为对象原型链的终点
  • undefined:

    • 变量被声明了,但没有赋值时,就等于 undefined
    • 调用函数时,应该提供的参数没有提供,该参数等于 undefined
    • 对象没有赋值属性,该属性的值为 undefined
    • 函数没有返回值时,默认返回 undefined

# 2、说说你有什么办法把数组去重?

  1. filter 过滤去重
  2. [...new Set(arr)]
  3. for 循环嵌套,利用 splice 去重
  4. 新建数组,利用 indexOf 或者 includes 去重
  5. 先用sort排序,然后用一个指针从第0位开始,配合 while 循环去重

举两个例子:

[1,2,3,1,'a',1,'a'].filter(function(item,index,array){
    return index===array.indexOf(item)
})
1
2
3
[...new Set([1,2,3,1,'a',1,'a'])]
1

更多请看JavaScript数组去重(12种方法) (opens new window)

# 3、有写过原生的自定义事件吗?

创建自定义事件

  • 使用 Event
  • 使用 customEvent (可以传参数)
  • 使用 document.createEvent('CustomEvent') 和 initEvent()
  1. 使用Event
let myEvent = new Event('event_name');
1
  1. 使用customEvent (可以传参数)
let myEvent = new CustomEvent('event_name', {
	detail: {
		// 将需要传递的参数放到这里
		// 可以在监听的回调函数中获取到:event.detail
	}
})
1
2
3
4
5
6

# 3. 使用document.createEvent('CustomEvent')和initEvent()

let myEvent = document.createEvent('CustomEvent');// 注意这里是为'CustomEvent'
myEvent.initEvent(
	// 1. event_name: 事件名称
	// 2. canBubble: 是否冒泡
	// 3. cancelable: 是否可以取消默认行为
)
1
2
3
4
5
6
  • createEvent:创建一个事件
  • initEvent:初始化一个事件

可以看到,initEvent可以指定3个参数

事件的监听

自定义事件的监听其实和普通事件的一样,使用addEventListener来监听:

button.addEventListener('event_name', function (e) {})
1

事件的触发

触发自定义事件使用 dispatchEvent(myEvent)。

例子:

// 1.
// let myEvent = new Event('myEvent');
// 2.
// let myEvent = new CustomEvent('myEvent', {
//   detail: {
//     name: 'lindaidai'
//   }
// })
// 3.
let myEvent = document.createEvent('CustomEvent');
myEvent.initEvent('selfEvent', true, true)

let btn = document.getElementsByTagName('button')[0]
btn.addEventListener('selfEvent', function (e) {
  console.log(e)
  console.log(e.detail)
})
setTimeout(() => {
  btn.dispatchEvent(myEvent)
}, 2000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 4、apply, call ,bind 三者的相同点及区别

相同点: .bind(), .call(), 和 .apply() 是 JavaScript 中用于改变函数执行上下文(即函数内部的 this 值)的方法,第一个参数都是 this 要指向的对象

不同点:

  • call 和 aplly 的区别只是在与参数不同,call 的第二个参数是目标函数的第一个参数,第三个是目标函数的第二个参数以此类推,apply 的第二个参数是个数组,数组里面的每一项一次是目标函数的参数
  • bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用

详细区别如下:

.bind() 方法:

  • .bind() 方法会创建一个新函数,并将指定的对象作为新函数的执行上下文(即绑定 this 值)。
  • .bind() 方法返回一个绑定了指定上下文的新函数,而不会立即执行原函数。
  • 可以通过 .bind() 方法绑定上下文,并传递参数给原函数。

示例:

function greet(name) {
  console.log(`Hello, ${name}! I'm ${this.name}.`);
}

const person = { name: 'John' };

const greetPerson = greet.bind(person);
greetPerson('Alice'); // 输出: Hello, Alice! I'm John.
1
2
3
4
5
6
7
8

.call() 方法:

  • .call() 方法立即调用原函数,并将指定的对象作为原函数的执行上下文(即绑定 this 值)。
  • 可以通过 .call() 方法指定上下文,并传递参数给原函数。

示例:

function greet(name) {
  console.log(`Hello, ${name}! I'm ${this.name}.`);
}

const person = { name: 'John' };

greet.call(person, 'Alice'); // 输出: Hello, Alice! I'm John.
1
2
3
4
5
6
7

.apply() 方法:

  • .apply() 方法立即调用原函数,并将指定的对象作为原函数的执行上下文(即绑定 this 值)。
  • .call() 方法不同的是,.apply() 方法接受一个数组作为参数,数组中的元素将作为参数传递给原函数。

示例:

function greet(name) {
  console.log(`Hello, ${name}! I'm ${this.name}.`);
}

const person = { name: 'John' };

greet.apply(person, ['Alice']); // 输出: Hello, Alice! I'm John.
1
2
3
4
5
6
7

总结:

  • .bind() 方法创建一个新函数并绑定上下文,不立即执行。
  • .call() 方法立即调用函数并绑定上下文,可以传递参数。
  • .apply() 方法立即调用函数并绑定上下文,接受一个参数数组。

选择使用哪种方法取决于需要何时绑定上下文以及是否需要传递参数。

# 5、请写出以下代码的输出结果

function Person (name, sex) {
  this.name = name
  this.sex = sex
  var evil = '我是私有属性'
  var pickNose = function () {
    console.log('我是私有方法')
  }
  this.drawing = function () {
    console.log('我是公有方法')  
  }
}
Person.fight = function () {
  console.log('我是静态方法')
}
Person.prototype.protoMethod = function () {
  console.log('构造函数的原型对象方法')
}
var p1 = new Person('xiaomao', 'cat')
console.log(p1.name)
console.log(p1.evil)
p1.drawing()
p1.pickNose()
p1.fight()
p1.protoMethod()
Person.fight()
Person.protoMethod()
console.log(Person.sex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

答案:

  • 'xiaomao'
  • undefined
  • '我是公有方法'
  • Uncaught TypeError: p1.pickNose is not a function
  • Uncaught TypeError: p1.fight is not a function
  • '构造函数的原型对象方法'
  • '我是静态方法'
  • Uncaught TypeError: Person.wc is not a function
  • undefined

解析:

  • name 为公有属性,实例访问它打印出'xiaomao'
  • evil 为私有属性,实例访问它打印出'undefined'
  • drawing 是共有(实例)方法,实例调用它打印出'我要画一幅国画'
  • pickNose 是私有方法,实例调用它会报错,因为它并不存在于实例上
  • fight 是静态方法,实例调用它报错,因为它并不存在于实例上
  • protoMethod 存在于构造函数的原型对象中,使用实例调用它打印出'构造函数的原型对象方法'
  • fight 存在于构造函数上的静态方法,使用构造函数调用它打印出'我是静态方法'
  • protoMethod 存在于构造函数的原型对象中,并不存在于构造函数中所以报错
  • sex 为公有(实例)属性,并不存在于构造函数上,使用构造函数访问它为undefined

# 6、CommonJS和ES6模块的区别?

  • CommonJS模块是运行时加载,ES6 Modules是编译时输出接口
  • CommonJS输出是值的拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变
  • CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;而ES6 Modules只能是字符串
  • CommonJS this指向当前模块,ES6 Modules this指向undefined
  • 且ES6 Modules中没有这些顶层变量:arguments、require、module、exports、__filename、__dirname

# 7、说说回流(重排)和重绘

1.浏览器渲染机制

  • 浏览器采用流式布局模型(Flow Based Layout)
  • 浏览器会把HTML解析成 DOM,把 CSS 解析成 CSSOM,DOM 和 CSSOM 合并就产生了渲染树(Render Tree)。
  • 有了 RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,合成布局树,最后把节点绘制到页面上。
  • 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一。

注意:上面说的是首先会生成 Render Tree,也就是渲染树,其实这还是 16 年之前的事情,现在 Chrome 团队已经做了大量的重构,已经没有生成 Render Tree 的过程了。而布局树的信息已经非常完善,完全拥有 Render Tree 的功能。

2.重绘

由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如 outline, visibility, color、background-color 等,重绘的代价是高昂的,因为浏览器必须验证 DOM 树上其他节点元素的可见性。

3.回流

回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及 DOM 中紧随其后的节点、祖先节点元素的随后的回流

4.浏览器优化

现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • width、height
  • getComputedStyle()
  • getBoundingClientRect()

所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。

5.减少重绘与回流

CSS:

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局
  • 避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。
  • 尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。
  • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
  • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。
  • 避免使用 CSS 表达式,可能会引发回流。
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如 will-change、video、iframe 等标签,浏览器会自动将该节点变为图层。
  • CSS3 硬件加速(GPU加速),使用 css3 硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘 。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

JavaScript:

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

# 8、在移动端中怎样初始化根元素的字体大小?

一个简易版的初始化根元素字体大小。页面开头处引入下面这段代码,用于动态计算 font-size:(假设你需要的1rem = 20px)

(function () {
  var html = document.documentElement;
  function onWindowResize() {
    html.style.fontSize = html.getBoundingClientRect().width / 20 + 'px';
  }
  window.addEventListener('resize', onWindowResize);
  onWindowResize();
})();
1
2
3
4
5
6
7
8
  • document.documentElement:获取 document 的根元
  • html.getBoundingClientRect().width:获取 html 的宽度(窗口的宽度)
  • 监听 window 的 resize 事件

一般还需要配合一个 meta 头:

<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-sacle=1.0, maximum-scale=1.0, user-scalable=no" />
1

# 9、animation有一个steps()功能符知道吗?

一句话介绍:steps() 功能符可以让动画不连续。

地位和作用:和贝塞尔曲线(cubic-bezier()修饰符)一样,都可以作为 animation 的第三个属性值。

和贝塞尔曲线的区别:贝塞尔曲线像是滑梯且有4个关键字(参数),而 steps 像是楼梯坡道且只有 number 和 position 两个关键字。

语法:

steps(number, position)
1
  • number: 数值,表示把动画分成了多少段
  • position: 表示动画是从时间段的开头连续还是末尾连续。支持start和end两个关键字,含义分别如下:
    • start:表示直接开始。
    • end:表示戛然而止。是默认值。

# 10、在项目中如何把http的请求换成https?

由于我在项目中是会对 axios 做一层封装,所以每次请求的域名也是写在配置文件中,有一个 baseURL 字段专门用于存储它,所以只要改这个字段就可以达到替换所有请求 http 为 https 了。

当然后面我也有了解到:

利用meta标签把http请求换为https:

<meta http-equiv ="Content-Security-Policy" content="upgrade-insecure-requests">
1

# 11、requestAnimationFrame有了解过吗?

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。对于JS动画,用requestAnimationFrame 会比 setInterval 效果更好。

# 12、平常工作中ES6+主要用到了哪些?

ES6:

  • Class
  • 模块import和export
  • 箭头函数
  • 函数默认参数
  • ...扩展运输符允许展开数组
  • 解构
  • 字符串模版
  • Promise
  • let const
  • Proxy、Map、Set
  • 对象属性同名能简写

ES7:

  • includes
  • **求幂运算符

ES8:

  • async/await
  • Object.values()和Object.entries()
  • padStart()和padEnd()
  • Object.getOwnPropertyDescriptors()
  • 函数参数允许尾部

ES9:

  • for...await...of
  • ...展开符合允许展开对象收集剩余参数
  • Promise.finally()
  • 正则中的四个新功能

ES10:

  • flat()
  • flatMap()
  • fromEntries()
  • trimStart和trimEnd
  • matchAll
  • BigInt
  • try/catch 中报错允许没有err异常参数
  • Symbol.prototype.description
  • Function.toString() 调用时呈现原本源码的样子

# 13、如何判断一个变量是对象还是数组?

判断数组和对象分别都有好几种方法,其中用prototype.toString.call()兼容性最好。

function isObjArr(value){
     if (Object.prototype.toString.call(value) === "[object Array]") {
            console.log('value是数组');
       }else if(Object.prototype.toString.call(value)==='[object Object]'){//这个方法兼容性好一点
            console.log('value是对象');
      }else{
          console.log('value不是数组也不是对象')
      }
}
1
2
3
4
5
6
7
8
9

ps:千万不能使用 typeof 来判断对象和数组,因为这两种类型都会返回 "object"。

# 14、通过reduce函数来实现简单的数组求和,示例数组[3,4,8,0,9];

let result=[3,4,8,0,9].reduce((total,value)=>{ //这两个参数是默认参数不用设置的
	return total+value
});
1
2
3

# 15、以下代码的输出结果

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }

obj.foo(); // 1
foo2(); // 2
obj2.foo2(); // 3
1
2
3
4
5
6
7
8
9
10
11

解析:

  • obj.foo()中的this指向调用者obj
  • foo2()发生了隐式丢失,调用者是window,使得foo()中的this指向window
  • foo3()发生了隐式丢失,调用者是obj2,使得foo()中的this指向obj2

# 16、如何添加、删除、移动、复制DOM节点

创建:

  • createTextNode() //创建文本节点
  • createElement() //创建元素节点
  • createDocumentFragment() //创建文档碎片

操作:

  • appendChild() //增加
  • removeChild() //删除
  • replaceChild() //替换
  • insertBefore() //插入

查找:

  • getElementById()
  • getElementByTagName()
  • getElementByName()

# 17、DOM事件中target和currentTarget的区别?

  • target是事件触发的真实元素
  • currentTarget是事件绑定的元素
  • 事件处理函数中的this指向是中为currentTarget
  • currentTarget和target,有时候是同一个元素,有时候不是同一个元素 (因为事件冒泡)
    • 当事件是子元素触发时,currentTarget为绑定事件的元素,target为子元素
    • 当事件是元素自身触发时,currentTarget和target为同一个元素。

例如:

<body>
   <ul id="box">
       <Li id="apple">苹果</Li>
       <li>香蕉</li>
       <li>桃子</li>
   </ul>
</body>
<script type="text/javascript">
   var box = document.getElementById('box');
   var apple = document.getElementById('apple');

   //直接绑定在目标元素apple上
   apple.onclick = function (e){  
       console.log(e.target);          //<li id="apple">苹果</li>
       console.log(e.currentTarget);    //<li id="apple">苹果</li>
       console.log(this);               //<li id="apple">苹果</li>
       console.log(e.target === e.currentTarget);      //true
       console.log(e.target === this);           //true
   } 

  //绑定在父元素box上(如果点击apple这个li时)
   box.onclick = function (e){
       console.log(e.target);           // <li id="apple">苹果</li>
       console.log(e.currentTarget);       //<ul id="box">...</ul>
       console.log(this);                  //<ul id="box">...</ul>
       console.log(e.currentTarget===this);      //true
       console.log(e.target === e.currentTarget);        //false
       console.log(e.target === this);           //false
   }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 18、说一下let、const、var 的区别和let、const的暂时性死区

区别:

  1. let/const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
  2. 相同作用域中,let 和 const 不允许重复声明,var 允许重复声明。
  3. const 声明变量时必须设置初始值
  4. const 声明一个只读的常量,这个常量不可改变。

暂时性死区:let、const 所声明的变量,只在命令所在的代码块内有效。和var不同的还有,let命令不存在变量提升,所以声明前调用变量,都会报错。

# 19、Map/Set、WeakMap,什么作用【描述】

map可以用对象做key,set做集合使用。WeakMap弱引用,防止内存泄露

# 20、以下代码将会输出什么,解释以下

let n = [10, 20]
    let m = n;
    let x = m;
    m[0] = 100;
    x = [30, 40];
    x[0] = 200;
    m = x;
    m[1] = 300;
    n[2] = 400;
    console.log(n, m, x) 

[100, 20, 400]
[200, 300]
[200, 300]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

数组是引用类型, 浅拷贝的只是内存地址,所以改变了 x 的值就会影响 m 的值 和 n 的值,

# 21、下面这段代码输出结果是什么?

const arr = [1,2,3,4,5,6]
arr.forEach(async (item) => {
    await sleep(item)
    console.log('after', item)
})
function sleep(item){
    return new Promise(res => {
        setTimeout(() => {
            res(console.log('before', item))
        }, 2000)
    })
}

/*
before 1
after 1
before 2
after 2
before 3
after 3
before 4
after 4
before 5
after 5
before 6
after 6
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 22、 写出代码输出值,并说明原因

function F() {
  this.a = 1;
}
var obj = new F();
console.log(obj.prototype);
1
2
3
4
5

undefined 构造函数实例一般没有 prototype 属性

# 23、weak-Set、weak-Map 和 Set、Map 区别?

Set

  • 成员不能重复
  • 只有健值,没有健名,有点类似数组。
  • 可以遍历,方法有add, delete,has

weakSet

  • 成员都是对象
  • 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
  • 不能遍历,方法有add, delete,has

Map

  • 本质上是健值对的集合,类似集合
  • 可以遍历,方法很多,可以干跟各种数据格式转换

weakMap

  • 直接受对象作为健名(null除外),不接受其他类型的值作为健名
  • 健名所指向的对象,不计入垃圾回收机制
  • 不能遍历,方法同get,set,has,delete

# 24、['1', '2', '3'].map(parseInt) what & why ?

  • 首先让我们回顾一下,map函数的第一个参数callback:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
1
  • 这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。

  • 而 parseInt 则是用来解析字符串的,使字符串成为指定基数的整数。

parseInt(string, radix)
1
  • 接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。

  • 了解这两个函数后,我们可以模拟一下运行情况:

    • parseInt('1', 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1
    • parseInt('2', 1) //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN
    • parseInt('3', 2) //基数为2(2进制)表示的数中,最大值小于3,所以无法解析,返回NaN

map函数返回的是一个数组,所以最后结果为[1, NaN, NaN]

# 25、如何正确判断this的指向?

如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。

但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

this 的指向可以按照以下顺序判断:

全局环境中的 this

浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window;

node 环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {};

是否是 new 绑定

如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。如下:

  • 构造函数返回值是 function 或 object,new Super()是返回的是Super种返回的对象。
  • 构造函数返回值不是 function 或 object。new Super() 返回的是 this 对象。
  • 案例如下:
function Super(age){
  this.age = age
}

let instance = new Super('26')
console.log(instance.age) // 26
1
2
3
4
5
6
function Super(age){
  this.age = age
  let obj = {a:'2'}
  return obj
}

let instance = new Super('26')
console.log(instance) // {a:'2'}
console.log(instance.age) // undefined
1
2
3
4
5
6
7
8
9
function Super(age){
  this.age = age
}

let instance = new Super('26')
console.log(instance) // {age:'26'}
console.log(instance.age) // 26
1
2
3
4
5
6
7

函数是否通过 call,apply 调用,或者使用了 bind 绑定,如果是,那么this绑定的就是指定的对象【归结为显式绑定】。

function info(){
  console.log(this.age)
}
var person= {
  age: 20,
  info
}
var age = 28
var info = person.info
info.call(person) // 20
info.apply(person) // 20
info.bind(person)() // 20
1
2
3
4
5
6
7
8
9
10
11
12

这里同样需要注意一种特殊情况,如果 call,apply 或者 bind 传入的第一个参数值是 undefined 或者 null,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node环境为global,浏览器环境为window)

function info () {
  // node 环境中:非严格模式 globao,严格模式 null
  // 浏览器环境中:非严格模式 window, 严格模式为 null
  console.log(this)
  console.log(this.age)
}
var person = {
  age: 20,
  info
}
var age = 28
var info = person.info
// 严格模式抛出错误
// 非严格模式,node 下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式,浏览器环境下输出28(因为全局的 age 回挂在 window 上)
info.call(null)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的隐式调用为: xxx.fn()

function info(){
  console.log(this.age)
}
var person= {
  age: 20,
  info
}
var age = 28
person.info() // 20,执行的是隐式绑定
info(); // 28 注意这里直接调用和赋值引用调用结果不同
1
2
3
4
5
6
7
8
9
10

默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

非严格模式: node环境,指向全局对象 global,浏览器环境,指向全局对象 window。

严格模式:执行 undefined

function info(){
  console.log(this.age)
}

var age = 28
// 严格模式浏览器环境和node环境都抛错
// 非严格模式:node下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式:浏览器环境下输出 28(因为全局的 age 回挂在 window 上)
info()
1
2
3
4
5
6
7
8
9

箭头函数的情况

箭头函数没有自己的this,继承外层上下文绑定的this。

let obj = {
  age: 20,
  info: function() {
    return () => {
	  console.log(this.age)
	}
  }
}
let person = {age: 28}
let info = obj.info()
info() // 20

let info2 = obj.info.call(person)
info2() // 28
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 26、说一说你对JS执行上下文栈和作用域链的理解?

JS执行上下文

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。执行上下文类型分为:

  • 全局执行上下文
  • 函数执行上下文

执行上下文创建过程中,需要做以下几件事:

  1. 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
  2. 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
  3. 确定 this 的值,即 ResolveThisBinding

作用域

作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的JavaScript》(上卷)

作用域有两种工作模型:词法作用域和动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。(with 和 eval 能够修改词法作用域,但是不推荐使用,对此不做特别说明)

作用域分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

JS执行上下文栈(后面简称执行栈)

执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。规则如下:

  • 首次运行JavaScript代码的时候,会创建一个全局执行的上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push当前执行栈的栈顶。
  • 当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。

作用域链

作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。

# 27、可迭代对象有哪些特点

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,换个角度,也可以认为,一个数据结构只要具有 Symbol.iterator 属性(Symbol.iterator 方法对应的是遍历器生成函数,返回的是一个遍历器对象),那么就可以其认为是可迭代的。

可迭代对象的特点:

  • 具有 Symbol.iterator 属性,Symbol.iterator() 返回的是一个遍历器对象
  • 可以使用 for ... of 进行循环
  • 通过被 Array.from 转换为数组

原生具有 Iterator 接口的数据结构:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

# 28、以下代码的输出结果

var a = 10;
function fn() {
    console.log(a);
}
function bar(f) {
    var a = 20;
    f();
}
bar(fn); // 10
1
2
3
4
5
6
7
8
9

函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。 函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。

有人说过要到父作用域中取,其实有时候这种解释会产生歧义。例如:

var x = 10;
function fn() {
  console.log(x);
}
function show(f) {
    var x = 20;
    (function () {
        f();
    })();
}
show(fn); //10
1
2
3
4
5
6
7
8
9
10
11

相比而言,用这句话描述会更加贴切——要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的**“静态作用域**”。

# sum(2, 3)实现sum(2)(3)的效果

要实现 sum(2, 3) 和 sum(2)(3) 的效果,可以使用柯里化(Currying)的技巧。柯里化是将一个接受多个参数的函数转化为一系列接受单个参数的函数的过程。

下面是一个示例实现:

function sum(a, b) {
  if (typeof b === 'undefined') {
    // 如果只传入一个参数,则返回一个接受第二个参数的函数
    return function (c) {
      return a + c;
    };
  }

  // 如果传入两个参数,则直接返回它们的和
  return a + b;
}

console.log(sum(2, 3)); // 输出: 5
console.log(sum(2)(3)); // 输出: 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中,sum 函数首先检查是否只传入了一个参数 a。如果是这样,它返回一个新的函数,该函数接受第二个参数 c,并返回 a + c 的结果。这样就可以实现 sum(2)(3) 的效果。

# Promise和Callback有什么区别?

Promise和Callback都是用于处理异步操作的机制,但它们有一些区别。

  1. 回调(Callback):
    • 回调是一种传递函数作为参数的方式,用于在异步操作完成后执行特定的逻辑。
    • 回调函数通常在异步操作完成时被调用,接收结果或错误作为参数。
    • 回调函数的执行顺序和上下文可能难以控制,尤其在处理多个嵌套的回调时,可能导致回调地狱(Callback Hell)的问题。
    • 回调函数没有内置的错误处理机制,需要手动处理错误。

示例使用回调的代码:

function fetchData(callback) {
  // 模拟异步操作
  setTimeout(() => {
    const data = 'Hello, world!';
    callback(null, data); // 传递结果给回调函数
  }, 1000);
}

fetchData((error, result) => {
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Result:', result);
  }
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. Promise:
    • Promise 是一种更为现代化的异步编程模式,它提供了一种更结构化和可读性更高的方式来处理异步操作。
    • Promise 是一个对象,代表一个异步操作的最终完成或失败的状态,并返回相应的结果或错误。
    • Promise 可以链式调用,通过 then() 方法处理操作成功的情况,通过 catch() 方法处理操作失败的情况。
    • Promise 提供了内置的错误处理机制,可以通过 catch() 方法捕获和处理错误,或者通过 finally() 方法执行清理操作。

示例使用 Promise 的代码:

function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      const data = 'Hello, world!';
      resolve(data); // 完成操作并传递结果
      // 或者在出现错误时使用 reject(error) 来拒绝 Promise
    }, 1000);
  });
}

fetchData()
  .then(result => {
    console.log('Result:', result);
  })
  .catch(error => {
    console.error('Error:', error);
  });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

总结:

  • 回调是传递函数作为参数的方式,用于处理异步操作,但容易导致代码难以维护和阅读。
  • Promise 是一种更为结构化和可读性更高的异步编程模式,提供了内置的错误处理和链式调用的能力,使代码更具表达力和可维护性。