资讯专栏INFORMATION COLUMN

详解一套面试题

leanxi / 1852人阅读

摘要:作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

span的display值,文本example的颜色
example

其实浏览器中,这张图的排列顺序,就很好的表示出了这个demo中的优先级关系:

优先级关系:内联样式 > ID 选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器。 ⚠️!important是个例外,优先级最高。

更详细的CSS优先级请查看MDN-优先级是如何计算的?

写一个满屏的品字

这就是考验一个布局的能力,没什么好说的,办法很多。我用的flex打个样。

class="top"

class="left"

class="right"

如下代码,写出执行结果
var fun = function(arr) {
  for(var i = 0; i< arr.length;i++) {
    setTimeout(function() {
      console.log(i);
    },0)
  }
  console.log(arr[i])
}
fun([1,2,3,4])

直接写答案就没什么意思了,借这个题先扯一下执行上下文作用域作用域链闭包

执行上下文
以下demo、图示、结论绝大部分来自这个网站,推荐阅读!在这里引用是为了让大家更好的理解,我确实讲不了这么好!!!

一段JavaScript的代码执行的时候,都会产生一个执行上下文(也就是执行环境)。多段代码执行就会产生多个执行上下文。

console.log(1);
// 这段代码的执行上下文就是--全局环境
function test() {
  console.log("test");
}
test();
// test() 执行上下文就是test--函数环境

JavaScript中的运行环境大概包括三种情况:

全局环境:JavaScript代码运行起来会首先进入该环境

函数环境:当函数被调用执行时,会进入当前函数中执行代码

eval(不建议使用,可忽略)

⚠️JavaScript引擎会以栈的形式来处理这些执行上下文,栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文

看下面这个demo,相信大家一看就懂了:

var color = "blue";

function changeColor() {
    var anotherColor = "red";

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}

changeColor();

这里面有全局上下文(Global Context)changeColor()上下文swapColors()上下文,它们进栈出栈如下图:

每一个执行上下文都有自己的生命周期:

对执行上下文总结一些结论:

单线程

同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待

全局上下文只有唯一的一个,它在浏览器关闭时出栈

函数的执行上下文的个数没有限制

每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。

作用域、作用域链与闭包

作用域与执行上下文是完全不同的两个概念。

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

⚠️JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

看一个demo:

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();

在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。

innerTestEC = {
    VO: {...},  // 变量对象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
}

至于这里面的VO AO 有兴趣的可以去上面那个网站里看看,这里不提,我觉得不妨碍大家理解。

简单说就是,在innerTest这个方法内,能拿到test()方法中的变量,也能拿到全局环境中的变量,这就形成了一个作用域链。

看到这里相信大家都知道了,闭包不就是这个东东嘛。

它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。
当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。

JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。

而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。

setTimeout

绕了一圈回到这个题,这个题中的setTimeout又在何时执行呢?

在这里,将会介绍另外一个特殊的队列结构,页面中所有由setTimeout定义的操作,都将放在同一个队列中依次执行。

而这个队列执行的时间,需要等待到函数调用栈清空之后才开始执行。即所有可执行代码执行完毕之后,才会开始执行由setTimeout定义的操作。而这些操作进入队列的顺序,则由设定的延迟时间来决定。

这个题中循环4次,每次往队列里加入一个console.log(i),它们引用的都是同一个i在循环结束时,i已经变成4了。

console.log(arr[i])就是undefined

答案undefined , 4 ,4 ,4 ,4
如下代码,写出执行结果
function person(name) {
  if(name) {
    this.name = name;
  }
  console.log(this.name);
}
person.prototype.name = "Tom";
var human = {
  person: person,
  name: "Cat"
}
person();
person("Jack");
new person();
new person("Rose");
human.person();
person.call(window)

person(), 作为函数直接调用,this指向windowthis.name = window.name=undefined

person("Jack") ,跟上面一样,this指向windowthis.name = window.name=name="Jack"

new person() ,作为构造函数调用,this指向新生成的对象,在自身没有找到this.name就会沿着原型链查找,所以this.name = person.prototype.name=Tom

new person("Rose"),与上面类似,区别在于传了name

human.person(),作为对象方法调用,this指向human=>human.name = "Cat"

person.call(window),用call方法将this指向window,⚠️这里最容易错❌,person("Jack") 已经将window.name="Jack"

答案: undefined、Jack、Tom、Rose、Cat、Jack
如下代码,写出执行结果
var a = window.a = "finget.github.io"
function hello(){
  console.log(a);
  var a = "hello";
  console.log(a);
  console.log(b);
  let b = "finget";
}
hello();

这个题比较简单,主要涉及的就是变量提升,和作用域链。坑点就是第一个console.log(a)到底是打印undefined还是finget.github.io

再看看作用域链那张图:

hello()方法中定义了一个var a = "hello",虽然在刚执行的时候,根据变量提升原则,a=undefined,但是它还是很有骨气的,只要自己有绝不往上找。那如果换成let a = "hello"呢?

来来来试一试:

var a = window.a = "finget.github.io"
function hello(){
  console.log(a);
  let a = "hello";
  console.log(a);
  console.log(b);
  let b = "finget";
}
hello();
暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = "abc"; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

上面的结果就很清楚了,直接报错,后面的也不执行。

提取url的参数,以key-value形式返回

完全考正则,自己恶补吧。没办法!

let getSearch = function(url) { 
    let matched = /^(?:https?://[^?]*?)(.*)/gi.exec(url) 
    return matched ? matched[1] : "" 
} 
// 递归函数,循环匹配search 
let searchFn = function (search, query) { 
    if (search) { 
        let matched = /(w+)=(w*)/g.exec(search) 
        if (matched) { 
            query[matched[1]] = decodeURIComponent(matched[2]) 
            searchFn(search.slice(matched.index + matched[0].length), query) 
        } 
    } 
} 
let parseUrl = function (url) { 
    let query = {} 
    searchFn(getSearch(url), query) 
    return query 
} 
let url = "http://localhost:3009/h5/test?recordID=161851&order=2"
console.log(parseUrl(url)) // => { recordID: "161851", order: "2" }
判断一个字符串中出现最多的字符,统计次数
function maxStr(str) {
    let map = {}
    for(let v of str) {
        map[v] = ~~map[v] + 1
    }
    // 这里就类似这种结构 map={a:1,b:1}, ~~map[v]就类似parseInt(),如果某一个字符第一出现就是0,=> 0+1,以此类推!
    
    // Object.values 能将一个对象的value返回成一个数组,再去最大值
    let max = Math.max(...Object.values(map))
    for (let key in map) {
        if (map[key] == max){
            return {[key]: max}
        }
    }
}
let str = "aasdfasd,asdfjaslkdfjiqjwioaklsdf,asd,lqwejrio1ji3wioqjroiqqewslkasm"
console.log(maxStr(str))
按位非运算符“~”
先看看w3c的定义:

位运算 NOT 由否定号(~)表示,它是 ECMAScript 中为数不多的与二进制算术有关的运算符之一。

位运算 NOT 是三步的处理过程:

把运算数转换成 32 位数字

把二进制数转换成它的二进制反码(0->1, 1->0)

把二进制数转换成浮点数

简单的理解,对任一数值 x 进行按位非操作的结果为 -(x + 1)

console.log("~null: ", ~null);       // => -1
console.log("~undefined: ", ~undefined);  // => -1
console.log("~0: ", ~0);          // => -1
console.log("~{}: ", ~{});         // => -1
console.log("~[]: ", ~[]);         // => -1
console.log("~(1/0): ", ~(1/0));      // => -1
console.log("~false: ", ~false);      // => -1
console.log("~true: ", ~true);       // => -2
console.log("~1.2543: ", ~1.2543);     // => -2
console.log("~4.9: ", ~4.9);       // => -5
console.log("~(-2.999): ", ~(-2.999));   // => 1

那么, ~~x就为 -(-(x+1) + 1) 相当于是 parseInt()

console.log("~~null: ", ~~null);       // => 0
console.log("~~undefined: ", ~~undefined);  // => 0
console.log("~~0: ", ~~0);          // => 0
console.log("~~{}: ", ~~{});         // => 0
console.log("~~[]: ", ~~[]);         // => 0
console.log("~~(1/0): ", ~~(1/0));      // => 0
console.log("~~false: ", ~~false);      // => 0
console.log("~~true: ", ~~true);       // => 1
console.log("~~1.2543: ", ~~1.2543);     // => 1
console.log("~~4.9: ", ~~4.9);       // => 4
console.log("~~(-2.999): ", ~~(-2.999));   // => -2
实现一个拷贝函数 JSON.parse()
const newObj = JSON.parse(JSON.stringify(oldObj));
️1.他无法实现对函数 、RegExp等特殊对象的克隆
2.会抛弃对象的constructor,所有的构造函数会指向Object
3.对象有循环引用,会报错
比较完善的深拷贝
我觉得面试手写这个也太那啥了!
const isType = (obj, type) => {
  if (typeof obj !== "object") return false;
  const typeString = Object.prototype.toString.call(obj);
  let flag;
  switch (type) {
    case "Array":
      flag = typeString === "[object Array]";
      break;
    case "Date":
      flag = typeString === "[object Date]";
      break;
    case "RegExp":
      flag = typeString === "[object RegExp]";
      break;
    default:
      flag = false;
  }
  return flag;
};

const getRegExp = re => {
  var flags = "";
  if (re.global) flags += "g";
  if (re.ignoreCase) flags += "i";
  if (re.multiline) flags += "m";
  return flags;
};

const clone = parent => {
  // 维护两个储存循环引用的数组
  const parents = [];
  const children = [];

  const _clone = parent => {
    if (parent === null) return null;
    if (typeof parent !== "object") return parent;

    let child, proto;

    if (isType(parent, "Array")) {
      // 对数组做特殊处理
      child = [];
    } else if (isType(parent, "RegExp")) {
      // 对正则对象做特殊处理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, "Date")) {
      // 对Date对象做特殊处理
      child = new Date(parent.getTime());
    } else {
      // 处理对象原型
      proto = Object.getPrototypeOf(parent);
      // 利用Object.create切断原型链
      child = Object.create(proto);
    }

    // 处理循环引用
    const index = parents.indexOf(parent);

    if (index != -1) {
      // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      // 递归
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};
查找素数 试除法

这种方式很传统理解上也简单,给定一个范围,那么就逐个循环去试除小于它数。

现在我们假设 N 等于 120

let N = 120;
let primes = [];
// 用于存素数结果集

loop:for(let x=2;x<=N;x++){
   for(let k=2;k
筛法

先把所有2的倍数去掉,然后剩下的那些数里面,最小的是3,3就是素数,然后把3的倍数都去掉,剩下的数里面,最小的是5,所以5也是素数…(可以看出已跳过4的试除,越多到后面跳过的数越多)

上述过程依次进行,但不像试除法逐个进行,就可以把某个范围内的非素数全都除去,剩下的就是素数了。这种方式的好处在于运算不重复,高效。

有一张很形象的动画,能直观地体现出筛法的工作过程。 (非素数就像被筛子筛掉一样)

let N = 120;
let primes = [];
// 用于存素数结果集
let nums = [];
// 待筛选的数据集
for(let x=2;x<=N;x++){
  //hooyes提示:此处初始化的时候,也可直接筛掉2的倍数数据减半。
  //if(x%2!==0)
   nums.push(x);
}
// 递归函数
function PrimeFn(data){

      let p = data.shift();
      // 数组最前端的一个数即素数,拿出来存起,并作为下次筛除的分母。
      primes.push(p);
      let t = [];
      for(let v of data){
         v%p!==0 ? t.push(v) : ""
         // 能被 p 整除的都筛除掉,不能整除的放到临时数组t存起来。
      }
      // t 是下次待筛数组,元素个数会越来越少,若还有就进行一次递归。
      t.length>0 ? PrimeFn(t) : ""

}
PrimeFn(nums);
console.log(primes.join(","));

/*
  得到小于N的素数集合
  2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113
*/
原文地址(https://hooyes.net/p/javascri...[https://hooyes.net/p/javascript-prime-number]
处理金额
/**
 * 金额三位一划分
 * @param  {[string/number]} money [金额]
 * @param  {[string/number]} round [小数位]
 * @param  {[any]}           flag  [是否四舍五入]
 * @return {[type]}       [description]
 */
function formatMoney(money,round,flag) {
    money = Number(money);
    round = Number(round);
    let formatReg = /(d)(?=(d{3})+.)/g;
    let sliceReg = new RegExp (`([0-9]+.[0-9]{${round}})[0-9]*`);
    if(!isNaN(money)&&Object.prototype.toString.call(money).slice(8,-1) === "Number") {
        if (!isNaN(round)&&flag) {
            return String(money.toFixed(round)).replace(formatReg,"$1,")
        } else if(!isNaN(round)){
            return String(money).replace(sliceReg,"$1").replace(formatReg,"$1,")
        } else if(round === "undefined"){
            return String(money).replace(formatReg,"$1,")
        } else {
            throw new Error("round is not Number!")
        }
    } else {
        throw new Error("money is not Number!")
    }
}

let res = formatMoney("1987562.12812",3,true)
console.log(res)
最后

创建了一个前端学习交流群,感兴趣的朋友,一起来嗨呀!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105114.html

相关文章

  • 求职准备 - 收藏集 - 掘金

    摘要:一基础接口的意义百度规范扩展回调抽象类的意义想不想通过一线互联网公司面试文档整理为电子书掘金简介谷歌求职记我花了八个月准备谷歌面试掘金原文链接翻译者 【面试宝典】从对象深入分析 Java 中实例变量和类变量的区别 - 掘金原创文章,转载请务必保留原出处为:http://www.54tianzhisheng.cn/... , 欢迎访问我的站点,阅读更多有深度的文章。 实例变量 和 类变量...

    cuieney 评论0 收藏0
  • 20W字囊括上百个前端面试的项目开源了

    摘要:字囊括上百个前端面试题的项目开源了这个项目是什么项目内容这个项目目前在上刚刚开源主要内容如下前端面试题主要整理了高频且有一定难度的前端面试题对这些面试题进行解读前端原理详解针对一些有一定难度面试题涉及的知识点进行详解比如涉及的编译原理响应式 20W字囊括上百个前端面试题的项目开源了 这个项目是什么? 项目内容 这个项目目前在GitHub上刚刚开源,主要内容如下: 前端面试题: 主要整...

    Euphoria 评论0 收藏0
  • 【推荐】最新200篇:技术文章整理

    摘要:作为面试官,我是如何甄别应聘者的包装程度语言和等其他语言的对比分析和主从复制的原理详解和持久化的原理是什么面试中经常被问到的持久化与恢复实现故障恢复自动化详解哨兵技术查漏补缺最易错过的技术要点大扫盲意外宕机不难解决,但你真的懂数据恢复吗每秒 作为面试官,我是如何甄别应聘者的包装程度Go语言和Java、python等其他语言的对比分析 Redis和MySQL Redis:主从复制的原理详...

    BicycleWarrior 评论0 收藏0
  • 【推荐】最新200篇:技术文章整理

    摘要:作为面试官,我是如何甄别应聘者的包装程度语言和等其他语言的对比分析和主从复制的原理详解和持久化的原理是什么面试中经常被问到的持久化与恢复实现故障恢复自动化详解哨兵技术查漏补缺最易错过的技术要点大扫盲意外宕机不难解决,但你真的懂数据恢复吗每秒 作为面试官,我是如何甄别应聘者的包装程度Go语言和Java、python等其他语言的对比分析 Redis和MySQL Redis:主从复制的原理详...

    tommego 评论0 收藏0

发表评论

0条评论

leanxi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<