1. new 的实现原理

  1. 新建一个空对象 let newObj = {}
  2. 修改对象的原型: 把新对象的 __proto__ 指向构造函数的 prototype 属性
  3. 改变 this 指向,并执行
  4. 返回对象里边的内容原来构造函数里边的内容
1
2
3
4
5
6
7
8
function _new () {
let obj = {};
// 取第一个参数,第一参娄就是构造函数
let [Constructor, ...args] = [...arguments];
obj.__proto__ = Constructor.prototype;
let result = Constructor.apply(obj, args);
return result === 'object' ? result : obj;
}

2. 如何正确判断一 this 指向

分析好上下文:谁调用指向谁

分四种情况:new, 显示,隐式,默认

参考这里: this指向

3. 深拷贝和浅拷贝的区别是什么? 实现一个深拷贝

浅拷贝一般指用来拷贝栈内存上的东西,而深拷贝一般是指用来拷贝堆内存上的东西

这是我个人的理解, 为啥这么说呢 ?

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

之前有文章介绍过基本类型是存在栈内存上的,而引用类型来说,对象的属性是保存在栈内存中的,属性对应的值是保存在堆内存中的, 同时,栈内存上也保存着该属性的值所在的堆内存的地址。

所以我们可以这样去理解,浅拷贝是栈内存上的拷贝,而深拷贝是堆内存上的拷贝.

首先我们要明月一个前提:

只有对象里嵌套对象的情况下,才会根据需求讨论,我们要深拷贝还是浅拷贝。

浅拷贝实现:

  1. Oject.assign()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let ahui = {
name: "ahui",
age: "18",
city: ["泰国","新加坡","印度尼西亚"],
sayname: function () {
return this.name
},
saycity: function () {
return this.city
}
}

// let angeli = Object.assign({}, ahui) // 这样实现深拷贝
let angeli = Object.assign(ahui, {}) // 这样实现浅拷贝

angeli.name = "angeli"

console.log(ahui.sayname()) // angeli
console.log(angeli.sayname()) // angeli

angeli.city = ["深圳","娄底"]

console.log(ahui.saycity()) // ["深圳","娄底"]
console.log(angeli.saycity()) // ["深圳","娄底"]

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

关于 Object.assign()这个方法是深拷贝还是浅拷贝的时候,是分情况的。

假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。也就是说,如果对象的属性值为简单类型(如stringnumber),通过Object.assign({},srcObj);得到的新对象为深拷贝;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝的。

深拷贝实现

  1. 可以使用各种第三方的库, 我常用 lodash 的 cloneDeep
  2. 通过判断加递归实现深拷贝
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function cloneDeep(obj) {
if (typeof obj === 'object') {
let tempObj = Array.isArray(obj) ? [] : {}

for (key in obj) {
// 意思就是__proto__上面的属性,我不拷贝
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {
tempObj[key] = cloneDeep(obj[key])
} else {
tempObj[key] = obj[key]
}
}
}

return tempObj;
}
return obj
}


let ahui = {
name: "ahui",
age: "18",
city: ["泰国","新加坡","印度尼西亚"],
sayname: function () {
return this.name
},
saycity: function () {
return this.city
}
}

let angeli = cloneDeep(ahui) // 这样实现深拷贝

angeli.name = "angeli"

console.log(ahui.sayname()) // ahui
console.log(angeli.sayname()) // angeli

angeli.city = ["深圳","娄底"]

console.log(ahui.saycity()) // ["泰国","新加坡","印度尼西亚"]
console.log(angeli.saycity()) // ["深圳","娄底"]

4. call/apply的实现原理是什么?

callapply 的功能是相同的,都是 改变 this 的执行,并立马执行函数。区别在于传参方式不同:

func.call(thisArg,arg1,arg2,...):第一个参数是 this 指向的对象,其它参数依次传入。
func.apply(thisArg,[argsArray]):第一个参数是 this 指向的对象,第二个参数是数组或类数组。

call

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 先使用call 试一下
let person = {
name: "person",
sayname: function () {
return this.name
}
}

console.log(person.sayname()) // person
let ahui = { name: "ahui" };
console.log(person.sayname.call(ahui)) // ahui

// 下边我们自己实现一个 call
Function.prototype.imitateCall = function(context = window) {
// 这里的 this, 是指原来对象里的方法, 比如下边的 person.sayname
context.fn = this
// slice(start, end) 方法可从已有的数组中返回选定的元素。
let args = [...arguments].slice(1)
// 执行调用的函数, 比如下边的 sayname
let result = context.fn(...args)
// 删除函数调用,要不然会改变 context
delete context.fn
// 返回执行结果
return result
}

console.log(person.sayname.imitateCall(ahui)) // ahui

// 再试一下有参数的例子

let Boy = function (name, age) {
this.name = name
this.age = age
this.sayInfo = function(name, age) {
return `my name is: ${name}, my age: ${age}`
}
}

angeli = new Boy("angeli", 18)

console.log(angeli.sayInfo("angeli", 18)) // 1 my name is: angeli, my age: 18

let huihui = {}

console.log(angeli.sayInfo.call(huihui, "huihui", 28)) // 2 my name is: huihui, my age: 28

console.log(angeli.sayInfo.imitateCall(huihui, "pengpeng", 38)) // 3 "my name is: pengpeng, my age: 38"

5. 柯里化函数实现

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function add() {
var _args = Array.prototype.slice.call(arguments)

// 这个东西的作用就是利用闭包函数的特性把每次执行之后的参数者保存好,再依次放入数组中
var _adder = function () {
_args.push(...arguments);
return _adder;
}

// 在 js 中,每次打印的时候,都会调用 toString 函数,所以,重写这里的 toSting 等函数执行完就会进行打印的时候, 就会调用里边的求和函数
// reduce 方法的参数为函数,且不可以少; array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
_adder.toString = () => _args.reduce((total, num) => total + num)

// 这个 add 函数返回的是一个函数,这点很重要,因为 add() 后面还其它的(), 这个意思就是执行函数的意思
return _adder;
}
add(1, 2, 3)(4)(5)

6. 如何让 (a == 1 && a == 2 && a == 3) 的值为 true

== 运算符会进行隐式转换

Object

==操作符会尝试通过方法valueOf和toString将对象转换为其原始值(一个字符串或数字类型的值)。

1
2
3
4
5
6
const a = {
i: 1,
// toString: () => a.i ++,
valueOf: () => a.i ++,
}
a == 1 && a == 2 && a == 3 // true

Array
对于数组对象,toString 方法返回一个字符串,该字符串由数组中的每个元素的 toString() 返回值调用 join() 方法连接(由逗号隔开)组成。

1
2
3
const a = [1, 2, 3]
a.join = a.shift
a == 1 && a == 2 && a == 3 // true

Symbol
Symbol对象被转为原始类型的值时,会调用 toPrimitive 方法,返回该对象对应的原始类型值

1
2
3
4
let a = {
[Symbol.toPrimitive]: (i => () => i++)(1)
}
a == 1 && a == 2 && a == 3 // true

Proxy
利用数据支持 Proxy/Object.definedProperty

1
2
3
4
5
6
7
let a = new Proxy({}, {
i: 1,
get: function() {
return () => this.i++
}
})
a == 1 && a == 2 && a == 3 // true

7. 什么是 BFC ? BFC 的布局规则是什么?如何创建 BFC ?

Box 是 CSS 布局的对象和基本单位,页面是由若干个Box组成的。

元素的类型 和 display 属性,决定了这个 Box 的类型。不同类型的 Box 会参与不同的 Formatting Context。

Formatting Context 是页面的一块渲染区域,并且有一套渲染规则,决定了其子元素将如何定位,以及和其它元素的关系和相互作用。

Formatting Context 有 BFC (Block formatting context),IFC (Inline formatting context),FFC (Flex formatting context) 和 GFC (Grid formatting context)。FFC 和 GFC 为 CC3 中新增。

BFC 布局规则

  1. BFC内,盒子依次垂直排列。
  2. BFC内,两个盒子的垂直距离由 margin 属性决定。属于同一个BFC的两个相邻Box的margin会发生重叠【符合合并原则的margin合并后是使用大的margin】
  3. BFC内,每个盒子的左外边缘接触内部盒子的左边缘(对于从右到左的格式,右边缘接触)。即使在存在浮动的情况下也是如此。除非创建新的BFC。
  4. BFC的区域不会与float box重叠。
  5. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  6. 计算BFC的高度时,浮动元素也参与计算。

怎样建仓 BFC

  1. 根元素
  2. 浮动元素(float 属性不为 none)
  3. position 为 absolute 或 fixed
  4. overflow 不为 visible 的块元素
  5. display 为 inline-block, table-cell, table-caption

BFC 的应用

  1. 防止 margin 重叠 (同一个BFC内的两个两个相邻Box的 margin 会发生重叠,触发生成两个BFC,即不会重叠)
  2. 清除内部浮动 (创建一个新的 BFC,因为根据 BFC 的规则,计算 BFC 的高度时,浮动元素也参与计算)
  3. 自适应多栏布局 (BFC的区域不会与float box重叠。因此,可以触发生成一个新的BFC)

8. 异步加载JS脚本的方式有哪些

  1. <script> 标签中增加 async(html5) 或者 defer(html4) 属性,脚本就会异步加载

    defer 和 async 的区别:

    1. defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在window.onload 之前执行;
    2. async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
    3. 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载
    4. 多个 async 脚本不能保证加载顺序
  2. 动态创建 script 标签

    动态创建的 script ,设置 src 并不会开始下载,而是要添加到文档中,JS文件才会开始下载。

  3. XHR 异步加载JS

9. ES5有几种方式可以实现继承?分别有哪些优缺点?

1. 原型链继承

原型链实现继承的思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

原型链的基本概念:当一个原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个指向另一个原型的指针。同时,另一个原型中也包含着一个指向另一个构造函数的指针。如果另一个原型是另一个类型的实例,此时实例和原型就构成了原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType () {
this.citys = ['北京', '上海', '深圳'];
}

function SubType () {
this.name = 'sub';
}

SubType.prototype = new SuperType();

var instance1 = new SubType();

instance1.citys.push('阿斯加德');
// instance1.citys = ['阿斯加德'];

console.log(instance1.citys) // ["北京", "上海", "深圳", "阿斯加德"]

var instance2 = new SubType();

console.log(instance2.citys) // ["北京", "上海", "深圳", "阿斯加德"]

原型链继承存在的问题

  1. 包含引用类型的原型属性会被所有实例共享,这会导致对一个实例的修改会影响到另一个实例。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。原先的实例属性就变成了现在的原型属性
  2. 在创建了类型的实例时,不能向超类型的构造函数中传递参数

上边代码中, 当 instance1.citys = ['阿斯加德']; 时会有一个完全不同的效果, 查看详情: js 原型链继承问题拓展

2. 借用构造函数继承

在子类型构造函数的内容调用超类型构造函数。

函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法可以在新创建的对象上执行构造函数

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType(name) {
this.name = name;
}

function SubType() {
SuperType.call(this, 'ahui');
this.age = 18;
}

var instance = new SubType()

console.log(instance.name, instance.age); // ahui 18

优点:

  1. 可以向超类传递参数
  2. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

  1. 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

3. 组合继承(原型链 + 借用构造函数)

使用原型链实现对原型属性的方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

面向对象实例属性和原型属性判别方法 hasOwnProperty(); 如果为true就是原型属性 否则就是实例属性。

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
31
32
33
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}

// 给超类型添加一个实例属性
SuperType.prototype.sayName = function () {
console.log(this.name)
}

// 通过借用构造函数继承实例属性
function SubType(name, age) {
SuperType.call(this, name);
this.age = age
}

// 通过原型链继承原型属性
SubType.prototype = new SuperType();

SubType.prototype.sayAge = function () {
console.log(this.age)
}

var instance1 = new SubType("ahui", 18)
instance1.colors.push('yellow')

instance1.sayAge() // 18
console.log(instance1.colors) // ["pink", "blue", "green", "yellow"]

var instance2 = new SubType("angelee", 28)
instance2.sayAge() // 28
console.log(instance2.colors) //  ["pink", "blue", "green"]
// colors 是实例属性,通过借用构造函数实现的继承, 所以instance2不会受影响

缺点:

无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

优点:

  1. 可以向超类传递参数
  2. 每个实例都有自己的属性
  3. 实现函数复用

原型式继承

非常简单的一种继承方式:通过Object.create()方法实现原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象(可以覆盖原型对象上的同名属性),在传入一个参数的情况下, Object.create()object() 方法的行为相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
name: "ahui",
citys: ["北京", "上海", "深圳"]
}

var person1 = Object.create(person);
person1.name = "angeli";
person1.citys.push('阿斯加德')
console.log(person1.citys) // ["北京", "上海", "深圳", "阿斯加德"]

var person2 = Object.create(person);
person2.name = "penghui"
console.log(person2.citys) // ["北京", "上海", "深圳", "阿斯加德"]

在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。

缺点:

同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnotherObject(original) {
var clone = Object.create(original);
clone.sayHi = function () {
console.log("hello");
}
return clone;
}

var person = {
name: "ahui"
}

var person2 = createAnotherObject(person)

person2.sayHi() // hello

寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。基本思路:

  不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

寄生组合式继承的基本模式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function inheritPrototype(subType, superType) {
var prototype = Object.create(subperType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype;
}

function SuperType(name) {
this.name = name;
this.citys = ["北京"];
}

function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}

SubType.prototype = new SuperType();
inheritPrototype(SubType, SuperType)

只调用了一次超类构造函数,效率更高。避免在 SuberType.prototype 上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。

因此寄生组合继承是引用类型最理性的继承范式。

10. 隐藏页面中的某个元素的方法有哪些?

完全隐藏: 元素从渲染树中消失,不占据空间。
视觉上的隐藏:屏幕中不可见,占据空间。
语义上的隐藏:读屏软件不可见,但正常占据空间。

完全隐藏:

  1. display: none;
  2. hidden: <div hidden></div>

视觉上的隐藏:

  1. postion
  2. transform
  3. width, height
  4. opacity
  5. visibility
  6. z-index
  7. clip-path: clip-path: polygon(0 0, 0 0, 0 0, 0 0)

语义上的隐藏
aria-hidden: <div aria-hidden = "true"></div>

11. let、const、var 的区别有哪些?

声明方式 变量提升 暂时性死区 重复声明 块作用域有效
var 不存在 允许 不是
let 不会 存在 不允许
Const 不会 存在 不允许

这里有一个非常重要的点即是:在JS中,复杂数据类型,存储在栈中的是堆内存的地址,存在栈中的这个地址是不变的,但是存在堆中的值是可以变得。

12. 说一说你对JS执行上下文栈和作用域链的理解

执行上下文

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

作用域

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

作用域有两种工作模型:词法作用域和动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。

作用域分为:

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

JS执行上下文栈(执行栈)

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

规则:

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

作用域链

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

13. 防抖函数的作用是什么?请实现一个防抖函数

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次(理论上不会被调用,因为触了间隔一直小于wait, 这里的一次应该是触发停止的时候),而节流的 情况会每隔一定时间(参数wait)调用函数。

函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时,事件处理函数不执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 防抖
function debounce(fn, wait) {
var timeout = null;
return function() {
if(timeout !== null){
clearTimeout(timeout);
}
timeout = setTimeout(fn, wait);
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

这里的现象是如果你鼠标一直在滚动,那么处理函数就一直不执行,滚动停止1秒后执行。
当持续触发scroll事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,也就是说在持续触发scroll事件的过程中,事件处理函数handle一直没有执行。

14. 节流函数的作用是什么?有哪些应用场景,请实现一个节流函数

函数节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 节流throttle代码(定时器):
var throttle = function(func, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function() {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

15. 什么是闭包?闭包的作用是什么?

16. 实现 Promise.all 方法

闭包的定义

《JavaScript高级程序设计》:

闭包是指有权访问另一个函数作用域中的变量的函数

《JavaScript权威指南》:

从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。

《你不知道的JavaScript》

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

17. 请实现一个 flattenDeep 函数,把嵌套的数组扁平化

18. 请实现一个 uniq 函数,实现数组去重

19. 可迭代对象有哪些特点

20. JSONP 的原理是什么?

参考地址:

  1. 原文地址
  2. js 基本类型与引用类型的区别
  3. 一篇文章彻底说清JS的深拷贝/浅拷贝
  4. Object.assign 是浅拷贝还是深拷贝
  5. 详解JS函数柯里化
  6. 如何让 (a == 1 && a == 2 && a == 3) 的值为true?
  7. 关于 Proxy