写点什么

2023 我的前端面试小结

作者:loveX001
  • 2023-05-19
    浙江
  • 本文字数:11899 字

    阅读完需:约 39 分钟

代码输出结果

function Person(name) {    this.name = name}var p2 = new Person('king');console.log(p2.__proto__) //Person.prototypeconsole.log(p2.__proto__.__proto__) //Object.prototypeconsole.log(p2.__proto__.__proto__.__proto__) // nullconsole.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错console.log(p2.constructor)//Personconsole.log(p2.prototype)//undefined p2是实例,没有prototype属性console.log(Person.constructor)//Function 一个空函数console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性console.log(Person.prototype.constructor)//Personconsole.log(Person.prototype.__proto__)// Object.prototypeconsole.log(Person.__proto__) //Function.prototypeconsole.log(Function.prototype.__proto__)//Object.prototypeconsole.log(Function.__proto__)//Function.prototypeconsole.log(Object.__proto__)//Function.prototypeconsole.log(Object.prototype.__proto__)//null
复制代码


这道义题目考察原型、原型链的基础,记住就可以了。

说一下 SPA 单页面有什么优缺点?

优点:
1.体验好,不刷新,减少 请求 数据ajax异步获取 页面流程;
2.前后端分离
3.减轻服务端压力
4.共用一套后端程序代码,适配多端
缺点:
1.首屏加载过慢;
2.SEO 不利于搜索引擎抓取
复制代码

为什么 0.1+0.2 ! == 0.3,如何让其相等

在开发过程中遇到类似这样的问题:


let n1 = 0.1, n2 = 0.2console.log(n1 + n2)  // 0.30000000000000004
复制代码


这里得到的不是想要的结果,要想等于 0.3,就要把它进行转化:


(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
复制代码


toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?


计算机是通过二进制的方式存储数据的,所以计算机计算 0.1+0.2 的时候,实际上是计算的两个数的二进制的和。0.1 的二进制是0.0001100110011001100...(1100 循环),0.2 的二进制是:0.00110011001100...(1100 循环),这两个数的二进制都是无限循环的数。那 JavaScript 是如何处理无限循环的二进制小数呢?


一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留 52 位,再加上前面的 1,其实就是保留 53 位有效数字,剩余的需要舍去,遵从“0 舍 1 入”的原则。


根据这个原则,0.1 和 0.2 的二进制数相加,再转化为十进制数就是:0.30000000000000004


下面看一下双精度数是如何保存的:


  • 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0 表示正数,占用 1 位

  • 第二部分(绿色):用来存储指数(exponent),占用 11 位

  • 第三部分(红色):用来存储小数(fraction),占用 52 位


对于 0.1,它的二进制为:


0.00011001100110011001100110011001100110011001100110011001 10011...
复制代码


转为科学计数法(科学计数法的结果就是浮点数):


1.1001100110011001100110011001100110011001100110011001*2^-4
复制代码


可以看出 0.1 的符号位为 0,指数位为-4,小数位为:


1001100110011001100110011001100110011001100110011001
复制代码


那么问题又来了,指数位是负数,该如何保存呢?


IEEE 标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于 JavaScript 的数字是双精度数,这里就以双精度数为例,它的指数部分为 11 位,能表示的范围就是 0~2047,IEEE 固定双精度数的偏移量为 1023


  • 当指数位不全是 0 也不全是 1 时(规格化的数值),IEEE 规定,阶码计算公式为 e-Bias。 此时 e 最小值是 1,则 1-1023= -1022,e 最大值是 2046,则 2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013

  • 当指数位全部是 0 的时候(非规格化的数值),IEEE 规定,阶码的计算公式为 1-Bias,即 1-1023= -1022。

  • 当指数位全部是 1 的时候(特殊值),IEEE 规定这个浮点数可用来表示 3 个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为 0 的时候表示 NaN;小数位为 0 时,当符号位 s=0 时表示正无穷,s=1 时候表示负无穷。


对于上面的 0.1 的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.


所以,0.1 表示为:


0 1111111011 1001100110011001100110011001100110011001100110011001
复制代码


说了这么多,是时候该最开始的问题了,如何实现 0.1+0.2=0.3 呢?


对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对 JavaScript 来说,这个值通常为 2-52,在 ES6 中,提供了Number.EPSILON属性,而它的值就是 2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为 0.1+0.2 ===0.3


function numberepsilon(arg1,arg2){                     return Math.abs(arg1 - arg2) < Number.EPSILON;        }        
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
复制代码

symbol 有什么用处

可以用来表示一个独一无二的变量防止命名冲突。但是面试官问还有吗?我没想出其他的用处就直接答我不知道了,还可以利用 symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols 外)遍历到,所以可以用来模拟私有变量。


主要用来提供遍历接口,布置了 symbol.iterator 的对象才可以使用 for···of 循环,可以统一处理数据结构。调用之后回返回一个遍历器对象,包含有一个 next 方法,使用 next 方法后有两个返回值 value 和 done 分别表示函数当前执行位置的值和是否遍历完毕。


Symbol.for() 可以在全局访问 symbol

代码输出问题

function A(){}function B(a){  this.a = a;}function C(a){  if(a){this.a = a;  }}A.prototype.a = 1;B.prototype.a = 1;C.prototype.a = 1;
console.log(new A().a);console.log(new B().a);console.log(new C(2).a);
复制代码


输出结果:1 undefined 2


解析:


  1. console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1;

  2. console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined;

  3. console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。

代码输出结果

function Foo(){    Foo.a = function(){        console.log(1);    }    this.a = function(){        console.log(2)    }}
Foo.prototype.a = function(){ console.log(3);}
Foo.a = function(){ console.log(4);}
Foo.a();let obj = new Foo();obj.a();Foo.a();
复制代码


输出结果:4 2 1


解析:


  1. Foo.a() 这个是调用 Foo 函数的静态方法 a,虽然 Foo 中有优先级更高的属性方法 a,但 Foo 此时没有被调用,所以此时输出 Foo 的静态方法 a 的结果:4

  2. let obj = new Foo(); 使用了 new 方法调用了函数,返回了函数实例对象,此时 Foo 函数内部的属性方法初始化,原型链建立。

  3. obj.a() ; 调用 obj 实例上的方法 a,该实例上目前有两个 a 方法:一个是内部属性方法,另一个是原型上的方法。当这两者都存在时,首先查找 ownProperty ,如果没有才去原型链上找,所以调用实例上的 a 输出:2

  4. Foo.a() ; 根据第 2 步可知 Foo 函数内部的属性方法已初始化,覆盖了同名的静态方法,所以输出:1


介绍 Loader

常用 Loader:


  • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;

  • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;

  • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;

  • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;

  • style-loader: 将 css 代码以<style>标签的形式插入到 html 中;

  • css-loader: 分析 @import 和 url(),引用 css 文件与对应的资源;

  • postcss-loader: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;

  • less-loader / sass-loader: css 预处理器,在 css 中新增了许多语法,提高了开发效率;


编写原则:


  • 单一原则: 每个 Loader 只做一件事;

  • 链式调用: Webpack 会按顺序链式调用每个 Loader;

  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

实现节流函数和防抖函数

函数防抖的实现:


function debounce(fn, wait) {  var timer = null;
return function() { var context = this, args = [...arguments];
// 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; }
// 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); };}
复制代码


函数节流的实现:


// 时间戳版function throttle(fn, delay) {  var preTime = Date.now();
return function() { var context = this, args = [...arguments], nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } };}
// 定时器版function throttle (fun, wait){ let timeout = null return function(){ let context = this let args = [...arguments] if(!timeout){ timeout = setTimeout(() => { fun.apply(context, args) timeout = null }, wait) } }}
复制代码

Promise 的基本用法

(1)创建 Promise 对象

Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。


Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject


const promise = new Promise(function(resolve, reject) {  // ... some code  if (/* 异步操作成功 */){    resolve(value);  } else {    reject(error);  }});
复制代码


一般情况下都会使用new Promise()来创建 promise 对象,但是也可以使用promise.resolvepromise.reject这两个方法:


  • Promise.resolve


Promise.resolve(value)的返回值也是一个 promise 对象,可以对返回值进行.then 调用,代码如下:


Promise.resolve(11).then(function(value){  console.log(value); // 打印出11});
复制代码


resolve(11)代码中,会让 promise 对象进入确定(resolve状态),并将参数11传递给后面的then所指定的onFulfilled 函数;


创建 promise 对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建 promise 对象;


  • Promise.reject


Promise.reject 也是new Promise的快捷形式,也创建一个 promise 对象。代码如下:


Promise.reject(new Error(“我错了,请原谅俺!!”));
复制代码


就是下面的代码 new Promise 的简单形式:


new Promise(function(resolve,reject){   reject(new Error("我错了!"));});
复制代码


下面是使用 resolve 方法和 reject 方法:


function testPromise(ready) {  return new Promise(function(resolve,reject){    if(ready) {      resolve("hello world");    }else {      reject("No thanks");    }  });};// 方法调用testPromise(true).then(function(msg){  console.log(msg);},function(error){  console.log(error);});
复制代码


上面的代码的含义是给testPromise方法传递一个参数,返回一个 promise 对象,如果为true的话,那么调用 promise 对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false的话,会调用 promise 对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks

(2)Promise 方法

Promise 有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。


  1. then()


当 Promise 执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise 创建完了,那该如何调用呢?


promise.then(function(value) {  // success}, function(error) {  // failure});
复制代码


then方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为resolved时调用,第二个回调函数是 Promise 对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此可以采用链式写法,即then方法后面再调用另一个 then 方法。


当要写有顺序的异步事件时,需要串行时,可以这样写:


let promise = new Promise((resolve,reject)=>{    ajax('first').success(function(res){        resolve(res);    })})promise.then(res=>{    return new Promise((resovle,reject)=>{        ajax('second').success(function(res){            resolve(res)        })    })}).then(res=>{    return new Promise((resovle,reject)=>{        ajax('second').success(function(res){            resolve(res)        })    })}).then(res=>{
})
复制代码


那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。


2. catch()


Promise 对象除了有 then 方法,还有一个 catch 方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。


p.then((data) => {     console.log('resolved',data);},(err) => {     console.log('rejected',err);     }); p.then((data) => {    console.log('resolved',data);}).catch((err) => {    console.log('rejected',err);});
复制代码


3. all()


all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected


javascriptlet promise1 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(1);    },2000)});let promise2 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(2);    },1000)});let promise3 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(3);    },3000)});Promise.all([promise1,promise2,promise3]).then(res=>{    console.log(res);    //结果为:[1,2,3] })
复制代码


调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个 promise 对象resolve执行时的值。


(4)race()


race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected


let promise1 = new Promise((resolve,reject)=>{    setTimeout(()=>{       reject(1);    },2000)});let promise2 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(2);    },1000)});let promise3 = new Promise((resolve,reject)=>{    setTimeout(()=>{       resolve(3);    },3000)});Promise.race([promise1,promise2,promise3]).then(res=>{    console.log(res);    //结果:2},rej=>{    console.log(rej)};)
复制代码


那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:


Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
复制代码


5. finally()


finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。


promise.then(result => {···}).catch(error => {···}).finally(() => {···});
复制代码


上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。


下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。


server.listen(port)  .then(function () {    // ...  })  .finally(server.stop);
复制代码


finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:


promise.finally(() => {  // 语句});// 等同于promise.then(  result => {    // 语句    return result;  },  error => {    // 语句    throw error;  });
复制代码


上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

事件是什么?事件模型?

事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。


事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:


  • DOM0 级事件模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。所有浏览器都兼容这种方式。直接在 dom 对象上注册事件名称,就是 DOM0 写法。

  • IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。

  • DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。

对执行上下文的理解

1. 执行上下文类型

(1)全局执行上下文


任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的 window 对象,并且设置 this 的值等于这个全局对象,一个程序中只有一个全局执行上下文。


(2)函数执行上下文


当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。


(3)eval函数执行上下文


执行在 eval 函数中的代码会有属于他自己的执行上下文,不过 eval 函数不常使用,不做介绍。

2. 执行上下文栈
  • JavaScript 引擎使用执行上下文栈来管理执行上下文

  • 当 JavaScript 执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。


let a = 'Hello World!';function first() {  console.log('Inside first function');  second();  console.log('Again inside first function');}function second() {  console.log('Inside second function');}first();//执行顺序//先执行second(),在执行first()
复制代码
3. 创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段


1)创建阶段


(1)this 绑定


  • 在全局执行上下文中,this 指向全局对象(window 对象)

  • 在函数执行上下文中,this 指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined


(2)创建词法环境组件


  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。

  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域


(3)创建变量环境组件


  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。


2)执行阶段 此阶段会完成对变量的分配,最后执行完代码。


简单来说执行上下文就是指:


在执行一点 JS 代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为 undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。


在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出 this、arguments 和函数的参数。


  • 全局上下文:变量定义,函数声明

  • 函数上下文:变量定义,函数声明,thisarguments

手写 bind、apply、call

// call
Function.prototype.call = function (context, ...args) { context = context || window;
const fnSymbol = Symbol("fn"); context[fnSymbol] = this;
context[fnSymbol](...args); delete context[fnSymbol];}
复制代码


// apply
Function.prototype.apply = function (context, argsArr) { context = context || window;
const fnSymbol = Symbol("fn"); context[fnSymbol] = this;
context[fnSymbol](...argsArr); delete context[fnSymbol];}
复制代码


// bind
Function.prototype.bind = function (context, ...args) { context = context || window; const fnSymbol = Symbol("fn"); context[fnSymbol] = this;
return function (..._args) { args = args.concat(_args);
context[fnSymbol](...args); delete context[fnSymbol]; }}

复制代码

对 rest 参数的理解

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组


function mutiple(...args) {  let result = 1;  for (var val of args) {    result *= val;  }  return result;}mutiple(1, 2, 3, 4) // 24
复制代码


这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:


function mutiple(...args) {  console.log(args)}mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
复制代码


这就是 … rest 运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

原型修改、重写

function Person(name) {    this.name = name}// 修改原型Person.prototype.getName = function() {}var p = new Person('hello')console.log(p.__proto__ === Person.prototype) // trueconsole.log(p.__proto__ === p.constructor.prototype) // true// 重写原型Person.prototype = {    getName: function() {}}var p = new Person('hello')console.log(p.__proto__ === Person.prototype)        // trueconsole.log(p.__proto__ === p.constructor.prototype) // false
复制代码


可以看到修改原型的时候 p 的构造函数不是指向 Person 了,因为直接给 Person 的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数 Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用 constructor 指回来:


Person.prototype = {    getName: function() {}}var p = new Person('hello')p.constructor = Personconsole.log(p.__proto__ === Person.prototype)        // trueconsole.log(p.__proto__ === p.constructor.prototype) // true

复制代码

强类型语言和弱类型语言的区别

  • 强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java 和 C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。

  • 弱类型语言:弱类型语言也称为弱类型定义语言,与强类型定义相反。JavaScript 语言就属于弱类型语言。简单理解就是一种变量类型可以被忽略的语言。比如 JavaScript 是弱类型定义的,在 JavaScript 中就可以将字符串'12'和整数 3 进行连接得到字符串'123',在相加的时候会进行强制类型转换。


两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。

new 操作符的实现原理

new 操作符的执行过程:


(1)首先创建了一个新的空对象


(2)设置原型,将对象的原型设置为函数的 prototype 对象。


(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)


(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。


具体实现:


function objectFactory() {  let newObject = null;  let constructor = Array.prototype.shift.call(arguments);  let result = null;  // 判断参数是否是一个函数  if (typeof constructor !== "function") {    console.error("type error");    return;  }  // 新建一个空对象,对象的原型为构造函数的 prototype 对象  newObject = Object.create(constructor.prototype);  // 将 this 指向新建对象,并执行函数  result = constructor.apply(newObject, arguments);  // 判断返回对象  let flag = result && (typeof result === "object" || typeof result === "function");  // 判断返回结果  return flag ? result : newObject;}// 使用方法objectFactory(构造函数, 初始化参数);
复制代码

原函数形参定长(此时 fn.length 是个不变的常数)

// 写法1-不保存参数,递归局部函数function curry(fn) {    let judge = (...args) => {        // 递归结束条件        if(args.length === fn.length) return fn(...args);        return (...arg) => judge(...args, ...arg);    }    return judge;}
// 写法2-保存参数,递归整体函数function curry(fn) { // 保存参数,除去第一个函数参数 let presentArgs = [].slice.call(arguments, 1); // 返回一个新函数 return function(){ // 新函数调用时会继续传参 let allArgs = [...presentArgs, ...arguments]; // 递归结束条件 if(allArgs.length === fn.length) { // 如果参数够了,就执行原函数 return fn(,,,allArgs); } // 否则继续柯里化 else return curry(fn, ...allArgs); }}
// 测试function add(a, b, c, d) { return a + b + c + d;}console.log(add(1, 2, 3, 4));let addCurry = curry(add);// 以下结果都返回 10console.log(addCurry(1)(2)(3)(4)); console.log(addCurry(1)(2, 3, 4));console.log(addCurry(1, 2)(3)(4));console.log(addCurry(1, 2)(3, 4));console.log(addCurry(1, 2, 3)(4));console.log(addCurry(1, 2, 3, 4));
复制代码

写版本号排序的方法

题目描述:有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']


实现代码如下:


arr.sort((a, b) => {  let i = 0;  const arr1 = a.split(".");  const arr2 = b.split(".");
while (true) { const s1 = arr1[i]; const s2 = arr2[i]; i++; if (s1 === undefined || s2 === undefined) { return arr2.length - arr1.length; }
if (s1 === s2) continue;
return s2 - s1; }});console.log(arr);
复制代码

什么是执行栈

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。 当开始执行 JS 代码时,根据先进后出的原则,后执行的函数会先弹出栈,可以看到,foo 函数后执行,当执行完毕后就从栈中弹出了。


平时在开发中,可以在报错中找到执行栈的痕迹:


function foo() {  throw new Error('error')}function bar() {  foo()}bar()
复制代码


可以看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的。当使用递归时,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题


function bar() {  bar()}bar()
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
2023我的前端面试小结_JavaScript_loveX001_InfoQ写作社区