手撕前端面试题javascript~文件扩展名分隔符单向绑定判断版本深浅拷贝内存泄露等

Posted 不良使

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕前端面试题javascript~文件扩展名分隔符单向绑定判断版本深浅拷贝内存泄露等相关的知识,希望对你有一定的参考价值。


前端的那些基本标签

html页面的骨架,相当于人的骨头,只有骨头是不是看着有点瘆人,只有HTML也是如此。

css,相当于把骨架修饰起来,相当于人的皮肉。

js(javascripts),动起来,相当于人的血液,大脑等一切能使人动起来的器官或者其他的。

在刷题之前先介绍一下牛客。Leetcode有的刷题牛客都有,除此之外牛客里面还有招聘(社招和校招)、一些上岸大厂的大佬的面试经验。 牛客是可以伴随一生的编程软件(完全免费),从学校到社会工作,时时刻刻你都可以用到感兴趣的可以去注册试试可以伴随一生的刷题app

刷题页面,功能完善,短时间坚持可看效果。

查看出现此处,筛选定制,查询指定大厂出现频率

首页功能强悍,完全免费

🍓🍓 直角三角形

问题 1:
请补全JavaScript代码,要求在页面上渲染出一个直角三角形,三角形换行要求使用"br"实现。三角形如下:

*
**
***

解答:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <div class='triangle'></div>

        <script>
            var triangle = document.querySelector('.triangle');
            // 补全代码
            let contents="";
            for(let i=1;i<4;i++)
                contents += "*".repeat(i)+ "<br/>";
            
            triangle.innerHTML=contents;
        </script>
    </body>
</html>

🎂🎂innerHTML
innerHTML在JS是双向功能:获取对象的内容 或 向对象插入内容;
如:
<div id="aa">这是内容</div>我们可以通过 document.getElementById(‘aa’).innerHTML 来获取id为aa的对象的内嵌内容;
也可以对某对象插入内容,如 document.getElementById(‘abc’).innerHTML=‘这是被插入的内容’; 这样就能向id为abc的对象插入内容

🎂🎂repeat
生成一个重复的字符串,有n个str组成,可修改为填充为数组等
例如:

let str="abc"
let str2=srt.repeat(2)
console.log(str2) 

结果:abcabc

总结:
🥭🥭1、核心步骤:
🍵🍵1)创建一个空字符串,用于存储HTML模板
🍵🍵2)外层循环为层数,内存循环为每层的"*"数
🍵🍵3)每当内层循环结束时,在字符串后方添加换行
🍵🍵4)innerHTML插入内容

除此之外,还有很多方法,例如 循环 -- 拼接字符串 -- 换行

<script>
            var triangle = document.querySelector('.triangle');
            // 补全代码
            var str = ""
            for(let i = 0; i < 3; i++) 
                // 外层循环控制行
                for(let j = 0; j <= i; j++) 
                    // 内层循环控制内容
                    str += "*"
                
                str += "<br/>"
            
            triangle.innerHTML= str
        </script>


🍓🍓文件扩展名

问题 2:
请补全JavaScript代码,要求以字符串的形式返回文件名扩展名,文件名参数为"filename"。

解答:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        
        <script>
            const _getExFilename = (filename) => 
                // 补全代码
                let index = filename.lastIndexOf('.')
                return filename.slice(0, index) && index !== -1 ? filename.slice(index) : '';
            
        </script>
    </body>
</html>

总结:

🥭🥭1、核心步骤。
1)首先通过lastIndexOf获取最后一个"."符号的位置
2)然后通过slice截取index(包括)之后的字符串

🥭🥭2、除此之外,还有很多方法,例如正则方法。

return /\\.(\\w)+/.exec(filename)[0];

其中:“exec() 方法用于检索字符串中的正则表达式的匹配



🍓🍓分隔符

问题 3:
请补全JavaScript代码,要求返回参数数字的千分位分隔符字符串。

输入:
_comma(12300)
输出:
‘12,300’

解答:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>
    	
        <script type="text/javascript">
            function _comma(number) 
                // 补全代码
                if (number < 1000) 
                    return number.toString();
                 else 
                    return _comma(Math.floor(number / 1000)) + "," + _comma(number % 1000);
                
            
        </script>
    </body>
</html>

总结:
🥭🥭1、核心步骤。
1)判断传入的number大小,根据大小走不同通道。
2)小于1000直接返回一个字符串,大于1000通过取模的方式判断是否要添加 ,【没记错的话,就跟大一学的水仙花数差不多】。

除此之外,还有很多方法,例如 正则

<script type="text/javascript">
            function _comma(number) 
               let str = number.toString();
                return  str.replace(/(\\d)(?=(?:\\d3)+$)/g, '$1,');
            
        </script>



🍓🍓单向绑定

问题 4:
请补全JavaScript代码,要求每当id为"input"的输入框值发生改变时触发id为"span"的标签内容同步改变。
注意:

1、 必须使用DOM0级标准事件(onchange)

解答:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>
    	<input id="input" type="text" />
        <span id="span"></span>

        <script type="text/javascript">
            // 补全代码
            var span = document.querySelector('#span')
            input.onchange = function () 
                span.innerText = this.value
            
        </script>
    </body>
</html>

总结:
🥭🥭1、核心步骤。
1)通过id绑定span标签。
2)onchange单向绑定。

🎂🎂onchange
onchange="JavaScript代码"
1、事件是在客户端改变输入控件的值,比如一个textbox,会出发的这个事件。
2、onchange 在元素值改变时触发。
3、onchange 属性适用于:
<input><textarea>** 以及 <select> 元素。**

oninput和onchange区别?
1、onchange事件还得等输入框失去焦点才触发。
2、oninput当输入框value值改变时就会触发。



🍓🍓创建数组

问题 5:
请补全JavaScript代码,要求返回一个长度为参数值并且每一项值都为参数值的数组。
注意:
1、 请勿直接使用for/while

解答:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>
    	
        <script type="text/javascript">
            const _createArray = (number) => 
                // 补全代码
                return new Array(number).fill(number)
            
        </script>
    </body>
</html>

总结
🥭🥭核心步骤
🍵🍵1)通过new Array函数构造实例的时候带入参数,可以生成该参数长度的空数组
🍵🍵2)通过Array.fill函数可以将数组的每一项都改编为参数值
🍵🍵3)或Array.from函数接收两个参数即可,第一个参数为数组或对象,都表示返回数组的长度。当参数为数组时它确定了返回的新数组长度,当参数为对象时,需要添加“length”属性表明数组长度
🍵🍵4)第二个参数为一个函数,即第一个数组参数中的每一项都调用该函数



🍓🍓判断版本

问题 5:
请补全JavaScript代码,该函数接收两个参数分别为旧版本、新版本,当新版本高于旧版本时表明需要更新,返回true,否则返回false。
注意:
1、版本号格式均为"X.X.X"
2、X∈[0,9]
3、当两个版本号相同时,不需要更新

解答:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>
    	
        <script type="text/javascript">
            const _shouldUpdate = (oldVersion, newVersion) => 
                // 补全代码
                oldVersion = oldVersion.split('.')
                newVersion = newVersion.split('.')
                return newVersion > oldVersion
            
        </script>
    </body>
</html>

总结
🥭🥭核心步骤
🍵🍵1)去除新旧版本号中的”.“
🍵🍵2)判断数字大小

如果需要严谨一点还需要转个类型。parseInt



🍓🍓什么是深拷贝,什么是浅拷贝,二者有什么区别,平时改怎么区分呢?

💖💖js中不同类型存放的位置不同

看完上面的例子你可能还是晕晕的,那么来详细的看看什么是深拷贝,什么是浅拷贝吧。

js中的基础类型:string,number,boolean,null,undefined,symbol
js中的引用类型:Object


基础类型:是按照值 存放在栈中,占用的内存空间的大小是确定的,并由系统自动分配和自动释放。
引用类型: 是按照地址 存在堆中,将存放在栈内存中的地址赋值给接收的变量。当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

需要注意的是,js和Java一样也有栈内存和堆内存,基础类型存放在栈内存中,引用类型存放在堆内存中。



💖💖深拷贝与浅拷贝的区别

深拷贝:主要是将另一个对象的属性值拷贝过来之后,另一个对象的属性值并不受到影响,因为此时它自己在堆中开辟了自己的内存区域,不受外界干扰。
浅拷贝:主要拷贝的是对象的引用值,当改变对象的值,另一个对象的值也会发生变化。

SO,需要注意的是。如果在对对象进行赋值时,如果不希望共享对象,那么就要进行深拷贝。

常用的深拷贝方法:
♻♻1、序列化和反序列

JSON.parse( JSON.stringify() ) 序列化和反序列

♻♻2、assign
es6新增的方法,可用于对象合并,将源对象的所有可枚举属性,复制到目标对象上。

Object.assign(target, source1, source2)

注意: 当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,
但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝

♻♻3、迭代递归方法
♻♻4、通过jQuery的extend方法实现深拷贝
♻♻5、lodash函数库实现深拷贝

let clone = cloneDeep(obj)





🍓🍓js中哪些操作会造成内存泄漏?

🥭🥭1.意外的全局变量

由于我们使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

🥭🥭2.被遗忘的计时器或回调函数。

当我们设置了setinterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

🥭🥭3.脱离DOM的引用

我们获取一个DOM元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

🥭🥭4.闭包

不合理的使用闭包,从而导致某些变量─直被留在内存当中。



看着是不是感觉手有点痒痒的了。那就来注册下自己试试吧,试试才知道好不好,试试才知道适不适合自己。就算最后没用这款软件那也尝试过,不后悔。
可以伴随一生的编程软件(完全免费),从学校到社会工作,时时刻刻你都可以用到感兴趣的可以去注册试试可以伴随程序员一生的app







觉得有用的可以给个三连,关注一波!!!带你玩转前端

前端面试题 ---- 手撕JavaScript call apply bind 函数(超详细)

手撕JavaScript call apply bind 函数

一、call apply bind 函数的基本用法

  首先这三个函数都是函数对象可用的三个方法,目的是为了改变调用者内部的 this 指向问题。下面简单介绍下他们的区别,详细内容可以看 其他文档

  例如我们定义如下 Person 函数:

function Person(){
	console.log(this.name)
}
Person();			// undefined
Person.call();		// undefined
Person.apply();		// undefined
let fun = Person.bind();
fun();				// undefined

  我们就可以利用 Person 这个函数对象去调用 call、apply、bind 这三个方法,这是为什么呢?其实这几个方法都在 Function 构造函数的原型对象上,而我们定义的 Person 构造函数就是 Function 构造函数的实例对象,Person 的原型 ( _proto_ ) 就是指向 Function 的原型对象的,所以可以通过原型链找到这三个方法。

  这三个函数都可以用于改变调用者内部的 this 指向的,第一个参数就是用于设置内部 this 所指的对象,之后可以有多个参数都将作为调用者的参数。

function Person(arg1,arg2){
	console.log(this.name+arg1+arg2);
}
let o = {
	name:"Dog"
}
Person("Arg1","Arg2");					// undefinedArg1Arg2
Person.call(o,"Arg1","Arg2");			// DogArg1Arg2
Person.apply(o,["Arg1","Arg2"]);		// DogArg1Arg2
let fun = Person.bind(o,"Arg1","Arg2");
fun();									// DogArg1Arg2

  从上述代码可以看出以下几点注意事项:

  call 和 apply 函数的调用会直接运行调用者函数,不同之处是他们传入参数的形式不一样,call 是将参数一个一个的传入apply 是将参数放入数组中传入

  bind 是暂时将调用者与要指定的 this 对象绑定在一起,但不执行调用者函数,可以暂时的保存这个绑定的函数,随后再执行。传参的形式与 call 一样。

  底层原理:

  为什么 Person.call(o) 就能将 Person 函数里的 this 指向 o 对象呢?其实这行代码相当于 o.Person()。即底层相当于在 o 对象中加入了一个 Person 方法。

function Person(){
	console.log(this.name);
}
let o = {
	name:"Dog"
}
Person.call(o);		// 相当于下面的代码
o.Person = function(){
	console.log(this.name);
};
o.Person();
/* 在调用 call 过程中 o 对象其实如下,之后 o 又恢复到原来的样子
o = {
	name:"Dog",
	Person:function(){
		console.log(this.name);
	}
}
*/

二、手写 myCall 方法

ES6 实现 myCall 方法

  • 确定函数位置

  因为我们要实现的 myCall 方法也是每个函数对象都能调用的方法,所以也要把这个方法写在 Function 的原型对象上。

Function.prototype.myCall = function(){
}
  • 确定函数参数

  在来考虑这个函数的参数,第一个参数是一个对象,用来修改调用者函数内部的 this 指向的对象,之后可以有多个参数,用于产给调用者函数作为参数的(这里可以用扩展操作符把第二个之后所有的参数都收集起来)。扩展操作符参考:10.6、参数扩展与收集

Function.prototype.myCall = function(obj,...args){
}
  • 确定函数内部的 this 指向

  这时,我们要明白一个事情就是在 myCall 内部的 this 指向什么?

  在除了箭头函数中的所有函数中 this 永远指向它的调用者。那么在我们后面要调用 myCall 的语句中例如: Person.myCall(o) 可以知道,调用 myCall 的调用者就是一个函数 Person ,所以 myCall 函数内部的 this 就是指向这个 Person 调用者函数

  • 为对象内部添加调用者函数

  接下来要做的就是将这个函数添加到传入的对象内部,让该对象拥有这个方法,可以调用。

Function.prototype.myCall = function(obj,...args){
    obj.fun = this;
}

  上述代码将调用 myCall 函数的调用者函数保存到了传入参数 obj 对象的内部 fun 属性(新建的属性)上,即现在的 obj 对象内部多了个 fun 方法,该方法名指向调用者函数。fun 这个属性名可以随便取,没有关系。

  • 调用外部的调用者函数

  obj 对象有了调用者函数后,因为 myCall 函数是要直接执行调用者函数的,所以在 myCall 内部直接执行该函数,这里需要将 myCall 第二个之后的参数作为调用者函数的参数传入,我们再次利用扩展操作符将 myCall 收集到的参数 args 数组展开。

Function.prototype.myCall = function(obj,...args){
    obj.fun = this;
    obj.fun(...args);
}
  • 删除内部的调用者函数,恢复原有结构

  执行完 fun 函数后,我们需要把这个属性给删除掉,不能改变了原 obj 对象的内部结构。

Function.prototype.myCall = function(obj,...args){
    obj.fun = this;
    obj.fun(...args);
    delete obj.fun;		// 删除添加的方法 fun 
}

  到这里,我们就基本实现了类似于 call 功能的 myCall 函数。不过还有一些细节地方需要处理。

  • 完善第一个参数问题

  问题一:就是当我们的第一个参数传入的是 null 时,我们自己写的 myCall 方法不能正常工作,而 call 可以。这时我们需要对参数 obj 进行一个处理。

Function.prototype.myCall = function(obj,...args){
    obj = obj || window;	// 当 obj 没传或传入 null 时,默认指向 window
    obj.fun = this;
    obj.fun(...args);
    delete obj.fun;		
}
  • 完善返回值问题

  问题二:还有一个问题就是调用者函数的返回值问题,如下第一段代码,这个时候我们上面所写的 myCall 方法不能很好的返回调用者函数的返回值。原 call 可以。

function Person(arg1,arg2){
	console.log(this.name+arg1+arg2);
    return 123;
}
let o = {
	name:"Dog"
}
Function.prototype.myCall = function(obj,...args){
    obj = obj || window;
    obj.fun = this;
    obj.fun(...args);
    delete obj.fun;		
}
let res1 = Person.myCall(o,"Arg1","Arg2");		// DogArg1Arg2
console.log(res1);    // undefined
let res2 = Person.call(o,"Arg1","Arg2");		// DogArg1Arg2
console.log(res2);    // 123
  • 完整版的 myCall 函数

  此时只需要将 myCall 内部调用外部调用者函数的结果保存并最终返回即可。如下第二段代码。

Function.prototype.myCall = function(obj,...args){
    obj = obj || window;
    obj.fun = this;
    let res = obj.fun(...args);		// 将调用者函数运行的结果保存
    delete obj.fun;
    return res;						// 将调用者函数运行的结果返回
}

  上述代码就是 ES6 中最终的手写 myCall 方法。

ES5 实现 myCall 方法

  用 ES5 实现 myCall 的主要问题是不能用 ES6 中的扩展语法实现参数的传递。这时主要就是去解决参数的传递问题。这时 myCall 的参数我们可以用内置的 arguments 来接收。但是 myCall 的第一个是不需要作为内部调用者函数执行的参数的。

Function.prototype.myCall = function(obj){
    obj = obj || window;
    obj.fun = this;
    // 将传入的第二个之后的参数要传入调用者函数,下行代码只是示例期望的样子,不符合语法的。
    let res = obj.fun(arguments[1],arguments[2],...);		
    delete obj.fun;
    return res;						// 将调用者函数运行的结果返回
}

  当不使用 ES6 的扩展语法时,我们可以采用 eval 函数(能够解析 JavaScript 代码)。

  我们需要将执行调用者函数的语句写成字符串的形式作为 eval 函数的参数传入如下示例,需要解决的问题是如何将 fun 的参数动态的生成如下的形式。

eval("obj.fun(arguments[1],arguments[2],...)");

  这里可以新建一个数组,用来保存 myCall 传入的第二个及之后的所有参数。如下代码:

Function.prototype.myCall = function(obj){
    obj = obj || window;
    obj.fun = this;
    const arrs = [];
    for(let i = 1;i<arguments.length;i++){
    	arrs.push(arguments[i]);
    }
}

   接下来需要将数组里的内容按, 分开插入eval 函数的相应位置即可。

  • 完整版的 myCall 函数
Function.prototype.myCall = function(obj){
    obj = obj || window;
    obj.fun = this;
    const arrs = [];
    for(let i = 1;i<arguments.length;i++){
    	arrs.push("arguments["+ i +"]");		
    }
    // 此时 arrs 就是:[arguments[1],arguments[2],...]
    eval("obj.fun("+ arrs +")");
    delete obj.fun;
    return res;						// 将调用者函数运行的结果返回
}

  这样我们 ES6 之前的手写 myCall 版本也就实现了。

  上述代码中要理解一个知识点就是。当把数组加一个空字符串打印时,就是把数组的所有元素以 , 逗号分割打印:

arr1 = [1,2,3,4];
console.log(arr1);		//  [1,2,3,4]
console.log(arr1+"");   // 1,2,3,4

三、手写 myApply 方法

  有了前面 myCall 方法的分析过程和 call 与 apply 函数的细微区别,我们可以很容易改造出自己的 myApply 方法。

  • ES6 之后版本
Function.prototype.myApply = function(obj,args){
    obj = obj || window;
    obj.fun = this;
    let res = null;
    if(!args){
    	res = obj.fun();
    }else{
    	res = obj.fun(...args);	
    }
    delete obj.fun;
    return res;						// 将调用者函数运行的结果返回
}
  • ES6 之前版本
Function.prototype.myCall = function(obj,args){
    obj = obj || window;
    obj.fun = this;
    const arrs = [];
    let res = null;
    if(!args){
    	res = obj.fun();
    }else{
    	for(let i = 0;i<args.length;i++){
    		arrs.push("args["+ i +"]");		
    	}
    	eval("obj.fun("+ arrs +")");
    }
    delete obj.fun;
    return res;						// 将调用者函数运行的结果返回
}

四、手写 myBind 方法

  bind 的不同之处是 bind 函数返回的是一个函数,而不执行调用者函数。第二是 bind 函数具有柯里化特性的即:参数可以在绑定 this 时传入一部分,在调用时再传入一部分。

function Person(a,b){
	console.log(this.name+a+b);
}
const o = {
	name:"Dog"
}
Person.bind(o,1)(2);    // Dog12

  上述代码中第七行的 Person.bind(o,1) 是一个函数,如果没有后面的括号2 (2) 就不会调用,有了括号才调用的,并且参数是在绑定 this 时传入了 a 为 1,调用时传入来了 b 为 2。

  在写自己的 myBind 方法时,内部的 this 绑定同 myCall 和 myApply 一样,主要解决返回值和柯里化特性。myBind 要返回一个函数,所以基本结构如下:

Function.prototype.myBind = function(){
	return function(){
	}
}

  上述代码中要分析清楚两个地方的 this 指向问题:

Function.prototype.myBind = function(){
    // 这里的 this 指向调用者函数
	return function(){
		// 这里的 this 指向 window
	}
}

  弄清楚上面的两个位置的 this 指向后,最终 myBind 函数是要返回调用者函数(可能修改内部的this)的。所以我们要在 return 的函数外部把外部的调用者函数保存起来(如下第 2 行)。然后在 return 返回的函数内部使用(这里就形成了闭包)。当然这里也可以 return 一个箭头函数,那么箭头函数里面的 this 就是外部环境中的 this 了,即调用者函数。

  绑定时的参数传入保存在数组 args1 中,如下第4行。调用时的参数保存在 args2 中,如下第 8 行。返回的函数中可以直接调用 apply 函数,并将外部传入的 obj 和所有的参数传入即可。

Function.prototype.myBind = function(obj){
    let that = this;		// 1.保存外部的调用者函数
    // 2. args1 以数组的形式保存绑定时传入的第二个之后的参数
    let args1 = Array.prototype.slice.call(arguments,1);	
    let args2 = null;	// 保存调用时传入的参数
	return function(){
        // 3. args2 以数组的形式保存调用时传入的参数
        args2 = Array.prototype.slice.call(arguments);
        // 4. 将 args1 和 args2 参数拼接后作为 apply 方法参数
		that.apply(obj,args1.concat(args2));
	}
}

以上是关于手撕前端面试题javascript~文件扩展名分隔符单向绑定判断版本深浅拷贝内存泄露等的主要内容,如果未能解决你的问题,请参考以下文章

手撕前端面试题JavaScript

手撕前端面试题javascript

手撕前端javascript面试题---快速排序 | 全排列 | instanceof

JavaScript手撕前端面试题:事件委托 | 判断URL是否合法 | 全排列

JavaScript手撕前端面试题:手写new操作符 | 手写Object.freeze

JavaScript手撕前端面试题:寄生组合式继承 | 发布订阅模式 | 观察者模式