目录
6、ES6语法中的const声明一个只读的常量,那为什么可以修改const的值?
1、手动实现一个bind、call、apply函数
bind实现:
// 简化实现,完整版实现中的第 2 步
Function.prototype._bind = function (context) {
var self = this;
// 第 1 个参数是指定的 this,截取保存第 1 个之后的参数
// arr.slice(begin); 即 [begin, end]
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 此时的 arguments 是指 bind 返回的函数调用时接收的参数
// 即 return function 的参数,和上面那个不同
// 类数组转成数组
var bindArgs = Array.prototype.slice.call(arguments);
// 执行函数
return self.apply( context, args.concat(bindArgs) );
}
}
call实现:
Function.prototype._call = function (thisArg, ...args) {
if (thisArg === null || thisArg === undefined) {
thisArg = window
} else {
thisArg = Object(thisArg)
}
thisArg.fn = this;
const result = thisArg.fn(...args)
delete thisArg.fn;
return result;
}
function fn() {
console.log(this.a);
}
const obj1 = {
a: 1
}
const obj2 = {
a: 2
}
fn._call(obj2)
apply实现:
Function.prototype._apply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window
context.fn = this
let result
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
function fn() {
console.log(this.a);
}
const obj1 = {
a: 1
}
const obj2 = {
a: 2
}
fn._apply(obj2)
2、高阶函数、函数柯里化
简单来说,高阶函数是一个接受函数作为参数传递或者将函数作为返回值输出的函数。
一个经常用到的柯里化函数:
var curry = function (fn) {
var args = [].slice.call(arguments, 1);
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
};
};
函数柯里化复杂的实现:
// 函数柯里化,指的是一个函数A,接收一个函数B作为参数,又返回一个函数C,这个函数C能处理函数A的剩余参数;
// 以下就是一个简单的函数柯里化,creatCurry为函数A,func为函数B,return出来了一个函数C
function sub_curry(func) {
// 以下代码的作用是得到A函数的剩余参数,
var args = [].slice.call(arguments, 1)
return function() {
var newArgs = args.concat([].slice.call(arguments))
return func.apply(this, newArgs)
}
}
function curry(fn, length) {
length = length || fn.length;
var slice = Array.prototype.slice;
return function() {
if (arguments.length < length) {
var combined = [fn].concat(slice.call(arguments));
return curry(sub_curry.apply(this, combined), length - arguments.length)
} else {
return fn.apply(this, arguments)
}
}
}
var fn = curry(function(a, b, c) {
return [a, b, c];
});
console.log(fn('a', 'b', 'c')); // ["a", "b", "c"]
console.log(fn('a', 'b')('c')); // ["a", "b", "c"]
console.log(fn('a')('b')('c')); // ["a", "b", "c"]
console.log(fn('a')('b', 'c')); // ["a", "b", "c"]
3、节流
函数节流,指的是在一定的时间间隔内(比如说3秒),只执行一次,在这三秒内无视后来产生的函数调用请求,也不会延长时间间隔。3秒间隔结束后,第一遇到新的函数调用会触发执行,然后在这新的3秒内依旧无视后来产生的函数调用请求,以此类推。
- 应用场景:非常适用于函数被频繁调用的场景,例如:window.onresize事件,mousemove事件,上传进度等等。
- 实现方案一:用时间戳来实现
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)
- 实现方案二:用定时器来实现
//原始函数
function add(a:any, b: any) {
console.log(a + b, '****本身调用', +new Date());
return a + b;
};
//节流函数
const throttle = (fn: any, wait = 50) => {
let timer: any = null;
return (...args: any) => {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, [...args])
timer = null;
}, wait)
}
}
}
// betterFn就是节流之后返回的新函数
const betterFn = throttle(add, 2000);
setInterval(() => {
// 如果在此处直接调用add,会出新add频繁触发的情况,加上throttle之后,明显情况好多了
betterFn(1, 1)
}, 200)
4、防抖
防抖函数debounce指的是某个 函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间3秒的函数,在这3秒内如果遇到函数调用请求就重新计时3秒,直至新的3秒内没有函数请求,此时执行函数,不然就以此类推重新计时。
//防抖
const debounce = (fn: any, wait = 50) => {
let timer: any = null;
return (...args: any) => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
};
// betterFn就是防抖之后返回的新函数
const betterFn = debounce(() => console.log('****执行了', +new Date()), 2000);
document.addEventListener('resize', betterFn)
5、null和undefined的本质区别是什么?
(1)从类型来说:
- null的数据类型是object,typeof null == object,会被隐式转换成0,不容易发现错误;
- undefined是一个基本数据类型,转换成数值为NaN;
(2)从变量赋值来说
- 给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果给对象的属性赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null,JS会回收全局变量为null的对象;
- 给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果给对象的属性赋值为undefined,说明这个值为空;
总结:
null,已经声明赋值为空;引用数据类型;数值转换成0。
undefined,已经声明未赋值;基本数据类型;数值转换成NaN。
6、ES6语法中的const声明一个只读的常量,那为什么可以修改const的值?
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = { name: '123' }; // TypeError: "foo" is read-only
解答:
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(number,string,boolean),值就是保存在变量指向的那个内存地址,因此等同于常量。
但是对于复合数据类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
因此将一个对象声明为常量必须非常小心。
7、JS数据类型
(1)分为两大类:基本数据类型和引用数据类型
- 基本数据类型:Number,String,Boolean,Null,Undefined,Symbol,BigInt
- 引用数据类型:Object(json,Array,function)
(2)两种数据类型存放机制
- 基本数据类型,体积小,放在栈内存里面
- 引用数据类型,体积大,放在堆内存里面
- 引用数据类型会有一个指针放在栈内存里面,用来定位堆里面存放的引用类型的数据
(3)如何判断这两种数据类型
- typeof 用来查找基本数据类型,引用类型除了function外,其他的都为object
- instanceof 用来查找引用数据类型;原理:instanceof在查找的时候会遍历左边变量的原型链,直到找到右边变量的prototype,找得到就返回true,找不到就返回false
- Object.prototype.toString().call() 所有数据类型,原型链和原型有关,首先toString()这个方法本来应该返回string的字符串形式,但是大多数情况会返回[object,****]这种形式,因为js重写了某些数据类型的toString方法;
const var1 = typeof 123;
const var2 = typeof '哈哈哈';
const var3 = typeof true;
console.log(var1); // number
console.log(var2); // string
console.log(var3); // boolean
const date1 = new Date()
const var4 = date1 instanceof Date;
const var5 = date1 instanceof Array;
console.log(var4); // true
console.log(var5); // false
const arr1 = [1, 2];
const obj1 = { name: '张三' };
const fn1 = () => {};
const str1 = 'I am a string';
const var6 = Object.prototype.toString.call(arr1);
const var7 = Object.prototype.toString.call(obj1);
const var8 = Object.prototype.toString.call(fn1);
const var9 = Object.prototype.toString.call(str1);
console.log(var6); // [object Array]
console.log(var7); // [object Object]
console.log(var8); // [object Function]
console.log(var9); // [object String]
(4)数据类型如何转换
- 转换为字符串:toString()/String()
- 转换为数字类型:Number()/ParseInt()/ParseFloat()
- 转换为布尔值:Boolean()/在实际的开发中,也会调用双感叹号强制转换成布尔类型
- 隐式转换:js是一门弱类型语言,运行期间,会根据运行环境自动类型转换
const var1 = String(123);
const var2 = !!'哈哈哈';
const var3 = Number(true);
const var4 = '人民' + 899;
console.log(var1); // '123'
console.log(var2); // true
console.log(var3); // 1
console.log(var4); // '人民899'
8、JS的事件循环(Event Loop)
- 同步任务:指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
- 异步任务:指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行;
(1)首先得解释一下单线程
js是单线程语言,用途决定了它必须是单线程语言;单线程指同一时间只能做一件事。
(2)然后解释一下代码执行流程
同步任务执行完毕,再去执行异步任务,异步任务分宏任务和微任务;如此循环往复,就构成了事件循环
(3)解释一下事件循环
常见的宏任务:setTimeout,setInterval,I/O操作,UI渲染等等
常见的微任务:Promise.then(回调),MutationObserver等等
执行顺序:
先执行同步任务、再执行微任务、最后执行宏任务(宏任务里面还有微任务,先执行微任务);同步任务、微任务、宏任务;同步任务、微任务、宏任务......如此循环往复.....
主线程任务----->微任务----->宏任务----->宏任务里的微任务----->宏任务里的微任务中的宏任务----->直到任务全部完成;
记住:要执行宏任务的前提是清空了所有微任务;
function test () {
console.log('start')
setTimeout(() => {
console.log('children2')
Promise.resolve().then(() => {console.log('children2-1')})
}, 0)
setTimeout(() => {
console.log('children3')
Promise.resolve().then(() => {console.log('children3-1')})
}, 0)
Promise.resolve().then(() => {console.log('children1')})
console.log('end')
}
test()
/*
以上代码在浏览器的执行顺序是:
* start
* end
* children1
* children2
* children2-1
* children3
* children3-1
* */
9、讲一下前端的深拷贝和浅拷贝
因为js的数据类型分两大类,基本数据类型和引用数据类型,在进行变量赋值的时候,分为下面两部分:
- 基本数据类型:赋值,赋值之后两个变量互不影响
- 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响
其实,简单来说,浅拷贝就是把变量A的指针赋值给了变量B,变量A和变量B本质上指向的是同一块内存;而深拷贝,是在内存中重新开辟出来一块新的空间给变量B。
看代码比较直观,如下所示:
let a = 'Jesus Loves You';
let b = a;
console.log(b);// Jesus Loves You
a = 'God Loves You'
console.log(a);// God Loves You
console.log(b);// Jesus Loves You
/*从上面的变动中可以看出,a的改动并没有影响到b,因为a为基本数据类型*/
在来看另外一组代码,如下所示:
let a = {
name: 'Holy Bible',
book: {
title: 'God Loves Everyone',
price: 45,
},
}
let b = a;
console.log(JSON.stringify(b));
// {"name":"Holy Bible","book":{"title":"God Loves Everyone","price":45}}
a.name = 'Other Book';
a.book.price = 55;
console.log(JSON.stringify(a));
// {"name":"Other Book","book":{"title":"God Loves Everyone","price":55}}
console.log(JSON.stringify(b));
// {"name":"Other Book","book":{"title":"God Loves Everyone","price":55}}
/*从上面的变动中可以看出,a的改动影响到了b,因为a为引用数据类型*/
(1)常用的浅拷贝方式:
- Object.assign()
- 展开语法Spread
- Array.prototype.slice()
(2)常用的深拷贝方式:
JSON.parse(JSON.stringify(object))
这个深拷贝的方式有bug,不建议在日常的开发中使用,会有以下几个问题:
会忽略undefined,会忽略symbol,不能序列化函数,不能解决循环引用的对象,不能正确处理new Date(),不能处理正则
10、讲一下JS的原型链
- 原型
在Js中,每当定义一个对象的时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype属性,这个属性指向函数的原型对象。
原型对象都会自动给获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。
当调用构造函数创建一个新实例后,高实例对象内部将包含一个指针(内部属性__proto__),指向构造函数的原型对象。
原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
- 原型链
原型链就是实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,有则返回,如果找不到,就去找原型的原型,直至查到最顶层Object函数的原型,其原型对象的__proto__已经没有可以指向的上层原型,因此其值为null,返回undefined。
原型链是实现继承的主要方法,其基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
11、说说JS的继承,如何实现继承?
1)继承是什么:
继承是面向对象软件技术当中一个重要的概念;如果一个类别B继承自另一个类别A,就把这个B称为A的子类,而把A称为B的父类别,或者A是B的超类;
- 继承可以使子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
- 在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。
2)实现继承的方法:
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
12、new创建一个对象的时候做了什么?
- new的作用:
new的作用是新创建一个对象,这个对象是从构造函数中实例化出来的一个新对象,并且这个对象继承了原对象(构造函数)的属性和方法。
- 调用new会发生以下几件事:
1)创建一个空对象obj,这个新对象的__proto__属性,指向构造函数的prototype属性;将对象与构造函数通过原型链连接起来;
2)此时构造函数的this会指向这个新对象;
3)执行构造函数中的代码,一般是通过this给新对象添加新的成员属性和方法;
4)最后返回这个新对象;
13、解释一下JS中this是如何工作的
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
总结:函数被调用时发生this绑定,this指向什么完全取决于函数在哪里被调用。
this的绑定规则一共有4种:
- 默认绑定(严格/非严格模式)
- 隐式绑定
- 显示绑定
- new绑定
独立函数调用:独立函数调用是,this使用默认绑定规则,默认绑定规则下this指向window(全局对象)。严格模式下:this无法使用默认绑定,this会绑定到undefined。
一、问题的由来
看一下下面两种写法,可能会有不一样的结果:
var obj = {
foo: function() {
}
};
var foo = obj.foo;
// 写法一
obj.foo();
// 写法二
foo();
上面代码中,虽然obj.foo和foo指向同一个函数,但是执行结果可能不一样。请看一下下面的例子:
var obj = {
bar: 1,
foo: function() {
console.log(this.bar);
}
};
var foo = obj.foo;
var bar = 2;
// 写法一
obj.foo();// 打印出来1
// 写法二
foo();// 打印出来2
这种差异的原因就在于:函数体内部使用了this关键字,this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this执行obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以两者的运行结果不一样。接下来看一下具体的原因:
二、内存的数据结构:
JS语言之所以有this的设计,跟内存里面的数据结构有关系。
var obj = { foo: 5 }
上面的代码将一个对象赋值给变量obj。JS引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。
也就是说,变量obj是一个地址(reference)。后面如果读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存:
{
foo: {
[[value]]: 5,
[[writable]]: true,
[[enumerable]]: true,
[[configurable]]: true,
}
}
注意,foo属性的值保存在属性描述对象的value属性里面。
三、函数
这样的结构是很清晰的,问题在于属性的值可能是一个函数:
var obj = { foo: function() {} }
这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。
{
foo: {
[[value]]: 函数的地址,
...
}
}
由于函数是一个单独的指,所以它可以在不同的环境(上下文)执行。
var f = function() {};
var obj = { f: f }
// 单独执行
f()
// obj环境执行
obj.f()
四、环境变量
JS允许在函数体内部引用当前环境的其他变量。
var f = function() {
console.log(x);
};
上面代码中,函数体里面使用了变量x。该变量由运行环境提供。
现在问题就来了,由于函数可以在不用的运行环境执行,所以就需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,他的设计的目的就是在函数体内部,指代当前的运行环境。
var f = function() {
console.log(this.x);
};
上面的代码中,函数体里面的this.x就是指当前运行环境的x。
var f = function() {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
}
// 单独执行
f() // 1
// obj环境执行
obj.f() // 2
上面代码中,函数f在全局环境执行,this.x指向全局环境的x。
在obj环境执行,this.x指向obj.x.
14、var、let、const区别和实现原理
三者的区别:
- var和let用来生命变量,const只用于声明只读的常量;
- var声明的变量不存在块级作用域,在全局范围内都有效,let和const声明的,只在它所在的代码块内有效;
- let和const不存在像var那样的「变量提升」现象,所以var定义变量可以先使用,后声明;而let和const只可先声明,后使用;
- let声明的变量存在暂时性死区,即只要块级作用域中存在let,那么它所声明的变量就绑定了这个区域,不再受外部的影响;
- let不允许在相同作用域内,重复声明同一个变量;
- const在声明时必须初始化赋值,一旦声明,其声明的值就不允许改变,更不允许重复声明;如const声明了一个复合类型的常量,其存储的是一个引用地址,不允许改变的是这个地址,而对象本身是可变的。
变量与内存之间的关系,主要由三个部分组成:
变量名、内存地址、内存空间
js引擎在读取变量时,先找到变量绑定的内存地址,然后找到地址所指向的内存空间,最后读取其中的内容。
当变量改变时,js引擎不会用新值覆盖之前旧值的内存空间(虽然从写代码的角度来看,确实像是被覆盖掉了),而是重新分配一个新的内存空间来存储新值,并将新的内存地址与变量进行绑定,js引擎会在合适的时机进行 GC (垃圾回收),回收旧的内存空间。
const定义常量以后,变量名与内存地址之间建立了一种不可变的绑定关系,阻隔变量地址被改变,当const定义的变量进行重新赋值时,根据前面的论述,js引擎会尝试重新分配新的内存空间,所以会被拒绝,便会抛出异常。
15、异步加载和延迟加载
- 异步加载
- 方案:动态插入script标签;
- 通过ajax去获取js代码,然后通过eval执行;
- script标签上添加defer或者async属性
- 创建并插入iframe,让他异步执行js;
- 延迟加载
有些js代码并不是页面初始化的时候就立刻需要的,而稍后的某些情况才需要的;
16、说说你对前端架构师的理解
- 负责前端团队的管理及与其它团队的协调工作,提升团队成员能力和整体效率;
- 带领团队完成研发工具及平台前端部分的设计、研发和维护;
- 带领团队进行前端领域前沿技术研究及新技术调研,保证团队的技术领先;
- 负责前端开发规范制定、功能模块制定、公共组件搭建等工作,并组织培训。
17、说说严格模式的限制
主要有以下限制:
- 变量必须要声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能用with语句
- 不能对制度属性赋值,否则报错
- 不能使用前缀0表示八进制,否则报错
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈