• 说一下闭包
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
使用闭包可以解决一个全局变量污染的问题。
如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。
什么是闭包?以及闭包的作用_fuyuyuki的博客-CSDN博客
1、this指向规则
(1)没有调用者,默认指向全局window,例如
fn();
(2)有调用者则指向调用者,例如
obj.fn();
(3)严格模式下,undefined
2、作用域
JS作用域:
作用域是可访问变量、对象、函数的集合。
(1)函数限定变量作用域
在javascript中,函数里面定义的变量,可以在函数里面被访问,但在函数外无法访问(局部作用域)
在ES5中,只有全局作用域和函数作用域,并没有块作用域(let和const);
(2)ES6作用域
作用域链:
先看函数里边有没有定义某个变量,没有就往外找(由内往外找)
变量提升:指的是变量的声明会提前
3、预解析
预解析做什么事?
把变量的声明提前了 ---- 提前到当前所在的作用域的最上面
函数的声明也会被提前 — 提前到当前所在域的最上面
在当前作用域中,JavaScript代码执行之前,浏览器首先会默认的把所有带var和function声明的变量进行提前的声明或者定义。
区别:
var : 提前声明,所以变量在赋值前调用是undefined
function:提前声明并定义,所以函数在声明前调用是正常输出
- num2 = 12; 相当于给window增加了一个num2的属性名,属性值是12;
- var num1 = 12; 相当于给全局作用域增加了一个全局变量num1,但是不仅如此,它也相当于给window增加了一个属性名num,属性值是12;
4、全局变量和局部变量同名的坑
全局变量是不会做用于同名局部变量的作用域的。
前端闭包相关面试题
问题1:什么是闭包
答:闭包就是能够读取其他函数内部变量的函数,通俗的讲就是函数a的内部函数b,被 函数a外部的一个变量引用的时候,就创建了一个闭包。
问题2:闭包应用场景
答:最常见的是函数封装的时候,再就是在使用定时器的时候,
即:当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。
它的最大用处有 两个,一个是它可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中
问题3:简单写一个闭包
答:
function a() {
var i=0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();//外部的变量
问题4:闭包的优缺点
优点:
① 减少全局变量;
② 减少传递函数的参数量;
③ 封装;
缺点:
① 使用闭包会占有内存资源,过多的使用闭包会导致内存溢出等
问题5:内存泄漏,解决方法
答:简单的说就是把那些不需要的变量,但是垃圾回收又收不走的的那些赋值为null,然后让垃圾回收走
问题6:如何使用
答:
1.定义外层函数,封装被保护的局部变量。
2.定义内层函数,执行对外部函数变量的操作。
3.外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。
• 什么是 js 的闭包?有什么作用,用闭包写个单例模式
1、什么是闭包?
闭包=函数+函数能够访问的自由变量
所以从技术的角度讲,所有 JS 函数都是闭包,但是这是理论上的闭包,还有一个实践角度上的闭包,从实践角度上来说,只有满足
(1)即使创建它的上下文已经销毁,它仍然存在;
(2)在代码中引入了自由变量,才称为闭包。
2、闭包的应用:
- 模仿块级作用域。
- 保存外部函数的变量。
- 封装私有变量。
3、单例模式:
单例模式是只允许实例化一次的对象类。
创建思路:
- 创建一个闭包函数
- 在闭包函数中创建保存实例的变量
instance
,并赋值null
- 编写单例函数
- 在闭包函数中返回单例的实例变量
- 返回前先判断
instance
是否为空,为空则先创建再返回
var Singleton = (function(){
var instance;
var CreateSingleton = function (name) {
this.name = name;
if(instance) {
return instance;
}
this.getName(); // 打印实例名字
return instance = this; //instance = this; return instance;
}
CreateSingleton.prototype.getName = function() { // 获取实例的名字
console.log(this.name)
}
return CreateSingleton;
})();
// 创建实例对象 1
var a = new Singleton('a');
// 创建实例对象 2
var b = new Singleton('b');
console.log(a===b);
JavaScript设计模式-单例模式_铛铛铛铛Huan的博客-CSDN博客
pink老师讲闭包
闭包容易引起歧义的地方
1、闭包一定有return吗?
答:不是。
解析:(1)简单理解:闭包 = 内层函数 + 引用的外层函数变量
先看个简单代码:
closure就是闭包
//普通闭包
function outer () {
const a = 1;
function f () {
console.log(a);
}
f();
}
outer();
(2)外部如果想要使用闭包里面的变量,则此时需要return
// 含有return的闭包
function outer () {
const a = 10;
return function () {
console.log(a);
}
}
const fn = outer();
fn()
2、闭包一定会有内存泄漏吗?
答:不是。
解析:谁会存在内存泄漏? count变量
借助于垃圾回收机制的标记清除法可以看出:
- result是一个全局变量,代码执行完毕也不会立即销毁;
- result使用fn函数;
- fn用到fun函数;
- fun函数里面用到count;
- count被引用就不会回收,所以一直存在;
- 此时,闭包引起了内存泄漏。
注意:
(1)不是所有的内存泄漏都要回收的;比如react里面很多闭包不能回收。
3、闭包应用:实现数据的私有,即变量私有化,防止全局变量被污染,外面的人可以用,但无法直接修改
// 比如,统计函数调用次数
// 此时,修改count的值修改的是全局变量,会改变统计次数
let count = 1
function fn() {
count++
console.log(`函数被调用了${count}次`);
}
fn() //2
fn() //3
// 实现数据私有,此时外面可以使用count值,但是无法直接修改
function fn() {
let count = 1
function fun() {
count++
console.log(`函数被调用了${count}次`);
}
return fun
}
const result = fn()
result() //2
result() //3
没有用闭包时,count值被修改导致调用次数出错:
用了闭包,count值不会被修改,函数可以正常调用输出:
4、闭包场景:节流,防抖,vue3,reacthooks
• 闭包 有什么用
(1)什么是闭包:
(2)为什么要用:
- 匿名自执行函数:我们知道所有的变量,如果不加上 var 关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用 var 关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。
- 结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
- 封装:实现类和继承等。
• 引用类型常见的对象
• es6 的常用
• class
说说var、let、const区别
常见的作用域: 全局作用域、函数作用域、块状作用域、动态作用域。
块级作用域:大部分包含{}的都可以构成块级作用域,但是函数和对象不构成块级作用域。
作用域链:内层作用域->外层作用域->…->全局作用域
对象 | 类型 |
---|---|
global/window | 全局作用域 |
function | 函数作用域(局部作用域) |
{ } | 块级作用域(ES6新增) |
this | 动态作用域 |
var、let、const区别 | |||
var | let(块级作用域) | const | |
ES5 | ES6 | ||
重复声明 | 允许 var a = 1; var a = 2; console.log(a); // 2 | 不允许 let a = 1; let a = 2; console.log(a); // error | 不允许 const a = 1; const a = 2; console.log(a); // error |
变量提升 | var会提升变量的声明到作用域的顶部 | 无 | |
暂时性死区 | 无 | 声明前不可用,函数内部和外部同时声明一个变量,以内部为准 | |
作用域 | 函数作用域 | 块级作用域 | |
widow对象的属性和方法 (全局作用域中) | 全局作用域中,var声明的变量和函数,会自动变window对象的变量 | 全局作用域中,let和const声明的变量和函数不会自动变为window的变量和对象 |
1. var声明变量存在变量提升
,let和const不存在
变量提升
2. let和const只能在块作用域里访问
3. 同一作用域下let和const不能声明同名变量,而var可以
4. const定义常量,而且不能修改,但是在定义的对象时对象属性值可以改变
总结:
var定义的变量,变量提升,没有块的概念,可以跨块访问。
let定义的变量,只能在块作用域里访问,不能声明同名变量。
const用来定义常量,使用时必须初始化(即必须赋值),不能声明同名变量,只能在块作用域里访问,而且不能修改,但是在定义的对象时对象属性值可以改变。
他们都不能跨函数访问
我的JavaScript笔记—— 一、var、let、const的区别?_admin_zlj的博客-CSDN博客
let和const区别(值是否可修改)
相同点:
① 只在声明所在的块级作用域内有效。
② 不提升,同时存在暂时性死区,只能在声明的位置后面使用。
③ 不可重复声明。
不同点:
① let声明的变量可以改变,值和类型都可以改变;
const声明的常量不可以改变,这意味着,const一旦声明,就必须立即初始化,不能以后再赋值。
打个比方说:
let x = 3;
x = 4;
这样子是可以的,因为let声明的变量是可以修改或者重新定义的。
再比如:
const x = 4;
x = 5;
这是错误的,因为const声明的变量是不可修改并且必须马上赋初值。
let和var区别
var与 let的区别:
var:函数作用域;存在变量提升;可重复定义;声明的变量会作为window的属性。
let:块级作用域;不存在变量提升(有暂时性死区);不可重复定义;声明的变量不会作为window的属性。
块级作用域:即在{}花括号内的域,由{ }包括,比如if{}块、for(){}块。
函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。
暂时性死区:在代码块中,在声明变量之前,该变量是不可用的。
JS中作用域有:全局作用域、函数作用域,是没有块作用域的。块级作用域是ES6中的。
var是js的,作用域是函数作用域的,let是ES6的,作用域是块级作用域的。
for循环推荐用let,因为
var是函数作用域,let是块作用域。
在函数中声明了var,整个函数内都是有效的,比如说在for循环内定义的一个var变量,实际上其在for循环以外也是可以访问的
而let由于是块作用域,所以如果在块作用域内定义的变量,比如说在for循环内,在其外面是不可被访问的,所以for循环推荐用let
ES5和ES6区别
区别:1、es6新增了箭头函数,es5没有;2、ES6中新增了块级作用域,es5没有;3、ES6引入Class概念,不再像ES5一样使用原型链实现继承;4、ES6中可以设置默认函数参数,es5不行;5、ES6中新增了promise特性。6、es6使用import导入模块,export导出;7、解构赋值。
总结:
- 解决原有语法上的一些不足,比如let 和 const 的块级作用域
- 对原有语法进行增强,比如解构、展开、参数默认值、模板字符串
- 全新的对象、全新的方法、全新的功能,比如promise、proxy、object的assign、is
- 全新的数据类型和数据结构,比如symbol、set、map
• 简单讲一讲 ES6 的一些新特性
块的对外接口,import 命令用于输入其他模块提供的功能
• 说一下类的创建和继承
(1)类的创建(es5):
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
this.age = 'aa';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
(2)类的继承——原型链继承
function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
(3)构造继承:
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
// 创建实例并传参
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
- instanceof 是 Java 的保留关键字。
- 作用是:测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
特点:可以实现多继承
(4)实例继承和拷贝继承
(5)组合继承:
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特点:可以继承实例属性/方法,也可以继承原型属性/方法
(6)寄生组合继承:
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}(function() {
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
较为推荐
• 面向对象的继承方式
原型链继承
构造继承
拷贝继承
寄生组合继承
• JS 中继承实现的几种方式,
• 说说前端中的事件流
• 说一下图片的懒加载和预加载
- 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
- 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
- 两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
- 懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
• mouseover 和 mouseenter 的区别
- mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是 mouseout
- mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是 mouseleave
• JS 的 new 操作符做了哪些事情
• JS 的各种位置,比如 clientHeight,scrollHeight,offsetHeight ,以及scrollTop,offsetTop,clientTop 的区别?
• JS 拖拽功能的实现
• 异步加载 JS 的方法
• Ajax 解决浏览器缓存问题
1.浏览器缓存的表现:
在项目中一般提交请求都会通过ajax来提交,但是发现,每次提交后得到的数据都是一样的,每次清除缓存后,就又可以得到一个新的数据。
2.浏览器缓存原因:
ajax能提高页面载入的速度主要的原因是ajax能实现局部刷新,通过局部刷新机制减少了重复数据的载入,也就是说在载入数据的同时将数据缓存到内存中,一旦数据被加载其中,只要没有刷新页面,这些数据就会一直被缓存在内存中,当我们提交 的URL与历史的URL一致时,就不需要提交给服务器,也就是不需要从服务器上面去获取数据。那么,我们得到还是最开始缓存在浏览器中的数据。虽然降低了服务器的负载提高了用户的体验,但是我们不能获取最新的数据。为了保证我们读取的信息都是最新的,我们就需要禁止他的缓存功能。
3.解决方法:
(1)在ajax发送请求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0")。
原理:If-Modified-Since:0 故意让缓存过期
(2)在ajax发送请求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache")。
原理:直接禁用缓存机制
(3)在URL后面加上一个随机数: "fresh=" + Math.random();。
原理:强行让每次的请求地址不同
(4)在URL后面加上时间搓:"nowtime=" + new Date().getTime();。
原理:强行让每次的请求地址不同
(5)如果是使用jQuery,直接这样就可以了$.ajaxSetup({cache:false})。
这样页面的所有 ajax 都会执行这条语句就是不需要保存缓存记录。
原理:不设置ajax缓存
• JS 的节流和防抖
在前端日常开发中会遇到一些频繁的事件触发,比如:
1. window 的 resize、scroll
2. mousedown、mousemove
3. input、keyup、keydown
这非常影响性能,所以我们需要控制它们触发的频率,方法就是防抖与节流。
throttle(节流):
连续触发事件,但是在n 秒中只执行一次函数
n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效(游戏中技能CD)
debounce(防抖):
触发事件后,在N秒后函数才会执行
n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时(游戏中的回城)
代码实现
节流(scroll事件监听)
这里添加了一个节流阀flag,在进入时判断flag是否为真,进入后立马将阀门关闭(flag=false)然后通过定时器控制在500ms后开起阀门,实现在500ms内的事件只触发一次
function throttle(fn,delay=100) {
let flag = true;
return function() {
if (flag) {
flag = false
}
setTimeout(() => {
fn.apply(this);
flag = true;
}, delay);
}
}
//ES6写法
function throttle(fn,wait){
let timer;
return (...args)=>{
if(!timer){
timer = setTimeout(()=>{
fn.apply(this,args)
timer = null
},wait)
}
}
}
防抖(表单验证,提交按钮,窗口resize)
定义一个timer变量在每次执行回调函数时先清空定时器,然后通过timer接收定时器,时间设置在500ms。(时间的话可以自己定)
也就是说在500ms内触发的定时器都被清除,只触发最后一次。
当间隔时间超过500ms时才触发函数。
function debounce(fn, delay=500) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this);
}, delay);
};
}
//ES6写法
function debounce(fn,wait){
let timer;
return (...args)=>{
timer && clearTimeout(timer)
timer = setTimeout(()=>{
fn.apply(this,args)
timer = null
},wait)
}
}
区别
相同点:
●都可以通过使用 setTimeout 实现
●目的都是,降低回调执行频率。节省计算资源
原理:函数节流与函数防抖的原理非常简单,巧妙地使用 setTimeout 来存放待执行的函数,这样可以很方便的利用 clearTimeout 在合适的时机来清除待执行的函数。
不同点:
节流(thorttle) | 防抖(debounce) | |
触发时 | 一定时间内只触发一次函数 | 延迟n秒后再执行函数 |
写法 | 变量timer | clearTimeout |
多次执行时 | 多次执行变成每隔一段时间执行 | 多次执行变成最后一次执行 |
侧重点 | 减少执行次数用节流 | 只执行一次用防抖 |
n秒内再次触发 | 只有一次生效 | 重新计时 |
应用场景 | scroll、search联想、submit | input、resize |
用户效果 |
|
|
应用场景
节流在间隔一段时间执行一次回调的场景有:
●滚动加载,加载更多或滚到底部监听。
●搜索框,search搜索联想功能。
●高频点击、表单重复提交。
防抖在连续的事件,只需触发一次回调的场景有:
●搜索框搜索输入。只需用户最后一次输入完,再发送请求
●手机号、邮箱验证输入检测
●窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
前端高薪必会的JavaScript重难点知识:防抖与节流详解 - 知乎
前端常见面试题之js函数的防抖和节流_函数防抖和节流面试_俗人844的博客-CSDN博客
完成节流可以使用时间戳与定时器的写法
• JS 中的垃圾回收机制
GC算法(Garbage Collection)
GC是什么?
垃圾回收机制的简写。可以找到内存中的垃圾,并释放和回收垃圾。
GC中的垃圾是什么?
程序中不再需要使用的对象
程序中不能再访问到的对象
GC算法是什么?
GC是一种机制,垃圾回收器完成具体的工作。
工作的内容就是查找垃圾释放空间、回收空间。
算法就是工作时查找和回收所遵循的规则。
什么是垃圾回收?
在说这个东西之前,先要解释什么是内存泄漏,因为内存泄漏了,所以引擎才会去回收这些没有用的变量,这一过程就叫垃圾回收
什么是内存泄漏?
程序的运行需要占用内存,当这些程序没有用到时,还不释放内存,就会引起内存泄漏。
回收的是什么?
回收内存。清理变量,释放内存空间
什么是内存管理?
内存管理:开发者主动申请空间、使用空间、释放空间
管理流程:申请 -> 使用 -> 释放
js的内存空间在定义变量时自动分配,程序猿无法指定明确大小
js内存生命周期
申请内存空间: let obj = {};
使用内存空间: (读写操作) obj.name = 'sunny';
释放内存空间: (js中并没有相应的释放api) obj = null;
js中的垃圾回收
js中的内存管理是自动的,每当我们创建函数、对象、数组的时候会自动的分配相应的内存空间;
- 对象不再被引用的时候是垃圾;
- 对象不能从根上访问到时也是垃圾;
标记清除法
-
核心:
-
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
-
就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
-
那些无法由根部出发触及到的对象被标记为不再使用,稍后进 行回收。
-
引用计数法
• eval 是做什么的
• 如何理解前端模块化
• 说一下 CommonJS、AMD 和 CMD
//定义模块
define(['dependency'], function() {
var name = 'Byron';
function printName(){
console.log(name);
}
return {
printName: printName
};
});
//加载模块
require(['myModule'], function (my) {
my.printName();
}
RequireJS 定义了一个函数 define,它是全局变量,用来定义模块:
define(id?dependencies?,factory)
在页面上使用模块加载函数:
require([dependencies],factory);
总结 AMD 规范:require()函数在加载依赖函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块加载成功,才会去执行。
因为网页在加载 JS 的时候会停止渲染,因此我们可以通过异步的方式去加载 JS,而如果需要依赖某些,也是异步去依赖,依赖后再执行某些方法。
-
总结
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
- CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
AMD,CMD,CommonJs到底是何方神圣?以及三者之间的区别_amd commonjs-CSDN博客
• 对象深度克隆的简单实现
因此当我们简单地执行复制操作时,实际是把地址指针进行了复制操作,因此在对象的实际数据改变之后,新旧对象都会受到影响。
function deepClone(obj) {
var newObj= obj instanceof Array ? []:{};
for(var item in obj){
var temple= typeof obj[item] == 'object' ? deepClone(obj[item]) : obj[item];
newObj[item] = temple;
}
return newObj;
}
ES5 的常用的对象克隆的一种方式。注意数组是对象,但是跟对象又有一定区别,所以
• 实现一个 once 函数,传入函数参数只执行一次
function once(func) {
var tag=true;
return function(){
if(tag==true){
func.apply(null,arguments);
tag=false;
}
return undefined
}
}
• JS 监听对象属性的改变
Object.defineProperty(user,'name',{
set:function(key,value){
}
})
Object.defineProperties(obj,{
a : {
configurable: true, // 设置属性可以更改,默认为false
set : function(value){}
},
b : {
configurable: true, // 设置属性可以更改,默认为false
set : function(value){}
}
}
})
(2)在 ES6 中可以通过 Proxy 来实现
var user = new Proxy({},{
set:function(target,key,value,receiver){
}
})
这样即使有属性在 user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化哦。
• 如何实现一个私有变量,用 getName 方法可以访问,不能直接访问
obj={
name:yuxiaoliang,
getName:function(){
return this.name
}
}
object.defineProperty(obj,"name",{
//不可枚举不可配置
});
(2)通过函数的创建形式
function product(){
var name='yuxiaoliang';
this.getName=function(){
return name;
}
}
var obj=new product();
• ==和===、以及 Object.is 的区别
1. == :等于,两边值类型不同的时候,先进行类型转换,再比较;
2. === :严格等于,只有当类型和值都相等时,才相等;
3. Object.is() :与 === 的作用基本一样,但有些许不同。
== 和 === 的区别
== 和 === 的最大区别就是前者不限定类型而后者限定类型。如下例,如果想要实现严格相等(===),两者类型必须相同。
1 == "1"; // true
1 === "1"; // false
0 == false; // true
0 === false; // false
对于严格相等,有以下规则,如果 x === y,那么:
a、如果x的类型和y的类型不一样,返回false;
b、如果x的类型是数字,那么:
(1):如果x是NaN,返回false;
(2):如果y是NaN,返回false;
(3):如果x和y是同一个数字值,返回true;
(4):如果x是+0,y是-0,返回true;
(5):如果x是-0,y是+0,返回true;
(6):其余返回false。
c、如果x和y的类型都为undefined或者null,返回true;
d、如果x和y的类型都为字符串类型,那么如果x和y是完全相同的字符编码序列,返回true,否则返回false;
e、如果x和y的类型都为布尔类型,那么如果x和y同为true或者false,返回true,否则返回false;
f、如果x和y是同一个对象值,返回true,否则返回false。
=== 和 Object.is() 的区别
Object.is() 的行为与 === 基本一致,但有两处不同:
a、+0 不等于 -0;
b、NaN 等于自身,即NaN==NaN。
https://www.cnblogs.com/zzh0318/p/12046530.html
• setTimeout、setInterval 和 requestAnimationFrame 之间的区别
定义:
相比 setTimeout
等 API 的优势之一是减少 DOM 重绘的次数。
使用
//控制台输出1和0
var timer = requestAnimationFrame(function(){
console.log(0);
});
console.log(timer);//1
cancelAnimationFrame方法用于取消定时器
//控制台什么都不输出
var timer = requestAnimationFrame(function(){
console.log(0);
});
cancelAnimationFrame(timer);
也可以直接使用返回值进行取消
var timer = requestAnimationFrame(function(){
console.log(0);
});
cancelAnimationFrame(1);
兼容
IE9-浏览器不支持该方法,可以使用setTimeout来兼容
【简单兼容】
if (!window.requestAnimationFrame) {
requestAnimationFrame = function(fn) {
setTimeout(fn, 17);
};
}
特点
【1】requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率
【2】在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的CPU、GPU和内存使用量
【3】requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销
总结:setTimeout 与 requestAnimationFrame 的区别:
引擎层面:
setTimeout 属于 JS 引擎,存在事件轮询,存在事件队列。
requestAnimationFrame 属于 GUI 引擎,发生在渲染过程的中重绘重排部分,与电脑分辨路保持一致。
性能层面:
https://www.cnblogs.com/xiaohuochai/p/5777186.html
• setTimeout(fn,100);100 毫秒是如何权衡的
• 用 setTimeout 来实现 setInterval
setInterval的缺点:
1、使用setInterval时,某些间隔会被跳过(如果上一次执行代码没有执行,那么这次的执行代码将不会被放入队列,会被跳过)
2、可能多个定时器会连续执行(上一次代码在队列中等待还没有开始执行,然后定时器又添加第二次代码,第一次代码等待时间和执行时间刚好等于第二次代码执行)
setTimeout实现setInterval功能
先来看一下两者的简介:setTimeout
在指定的毫秒数后调用函数或计算表达式。setInterval
按照指定的周期(以毫秒计)来调用函数或计算表达式。
简单理解为setTimeout
只执行一次,而setInterval
可以按周期一直执行下去。
使用递归函数实现,不断地去执行 setTimeout 从而达到 setInterval 的效果。这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,解决了 setInterval 的问题。
请看代码:
function newIntervel() {
setTimeout(function() {
console.log(123)
newIntervel()//调用自身
}, 1000)
}
newIntervel()//控制台每过一秒一直打印 123
函数newIntervel
内部有一个setTimeout
,每过一秒再次调用newIntervel
,这样就实现了基本的setInterval
功能。
封装
setInterval
方法我们一般是以函数作为第一个参数,时间作为第二个参数,就像这样setInterval(function() {}, 1000)
,所以我们用setTimeout
来封装一个可以传两个参数的newInterval
:
function newInterval(callback, time) {
setTimeout(function () {
callback()
newInterval(callback, time)
}, time)
}
使用方法:
newInterval(function () { }, 1000)
,这样就实现了比较完整的setInterval
。
用setTimeout实现setInterval功能 - 简书
• JS 怎么控制一次加载一张图片,加载完后再加载下一张
只要能监控到图片是否加载完成 就能实现了 要把图片当成是图片对象才行;
方法一、
借助Image对象的onload事件,该事件在图片对象加载完成时触发
<div id="mypic">onloading……</div>
var obj=new Image(); // <img> 标签每出现一次,一个 Image 对象就会被创建。
obj.src="http://www.phpernote.com/uploadfiles/editor/201107240502201179.jpg";
// onload当图像装载完毕时调用的事件句柄。
obj.onload=function(){ // 这个就是加载完成,当加载完成之后可以执行一个函数比如
alert('图片的宽度为:bai'+obj.width+';图片的高度为:'+obj.height);
document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />";
}
方法二、
同理,借助图片对象身上的complete属性可以查看图片是否加载完成
<div id="mypic">onloading……</div>
let obj = new Image()
obj.src = '..'
obj.onreadystatechange = function () {
if(this.readyState === 'complate') {
document.getElementById('mypic').innerHTML = '<img src=" '+this.src +'">'
}
}
onreadystatechange:属性指向一个回调函数。当页面的加载状态发生改变的时候readyState属性就会跟随发生变化,而这时readystatechange属性所对应的回调函数就会自动被调用
readyState:是一个只读属性,用一个整数和对应的常量来表示XMLHttpRequest请求当前所处的状态一般会在onreadystatechange事件的回调函数中,通过判断readyState属性的值,进而执行不同状态对应的函数。
语法:
xhr.onreadystatechange = function() {
if(xhr.readyState == n){
// 执行对应的函数
}
}
onload和onreadstatechange_onreadystatechange和onload-CSDN博客
• 代码的执行顺序
js代码的执行顺序_js代码执行的先后顺序_半个开心果的博客-CSDN博客
宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
process.nextTick 中的回调是在当前tick执行完之后,下一个宏任务执行之前调用的。
setTimeout(function(){console.log(1)},0);
new Promise(function(resolve,reject){
console.log(2);
resolve();
}).then(function(){console.log(3)
}).then(function(){console.log(4)});
process.nextTick(function(){console.log(5)});
console.log(6);
//输出2,6,5,3,4,1
注意:定义promise的时候,promise构造部分是同步执行的
首先分析Job queue的执行顺序:
script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering
I) 主体部分: 定义promise的构造部分是同步的,
因此先输出2 ,主体部分再输出6(同步情况下,就是严格按照定义的先后顺序)
II)process.nextTick: 输出5
III)promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是3,4
IV) setTimeout : 最后输出1
综合的执行顺序就是: 2——>6——>5——>3——>4——>1
• Eventloop
- microtasks 队列并完成里面的所有任务后再执行 macrotask 的任务。
- macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
- microtasks: process.nextTick, Promise, MutationObserver
• setTimeout 和 Promise 的执行顺序
setTimeout(function () {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if(i === 10) {console.log(10)}
i == 9999 && resolve();
}
console.log(3)
}).then(function() {
console.log(4)
})
console.log(5);
输出答案为 2 10 3 5 4 1
• 如何实现 sleep 的效果(es5 或者 es6)
简要介绍:在多线程编程中,sleep的作用是起到挂起的作用,使线程休眠,而js是单线程的,我们如何在js中模拟sleep的效果呢~ 也就是如何用同步的方式来处理异步。
序:为什么不能用setTimeout来实现sleep的效果
因为sleep要实现的是同步进程或者说同步程序的挂起,而setTimeout本身是异步的
1、while循环的方式
执行sleep()之后,休眠了1000ms之后输出了1111。以下循环的方式缺点很明显,容易造成死循环。
function sleep(ms){
var start=Date.now(),expire=start+ms;
while(Date.now()<expire);
console.log('');
return;
}
2、通过promise实现
function sleep(ms){
var temple=new Promise(
(resolve)=>{
console.log(111);setTimeout(resolve,ms)
});
return temple
}
sleep(500).then(function(){
//console.log(222)
})
//先输出了111,延迟500ms后输出222
3、通过async封装
function sleep(ms){
return new Promise((resolve)=>setTimeout(resolve,ms));
}
async function test(){
var temple=await sleep(1000);
console.log(1111)
return temple
}
test();
//延迟1000ms输出了1111
4、通过generate实现
function* sleep(ms){
yield new Promise(function(resolve,reject){
console.log(111);
setTimeout(resolve,ms);
})
}
sleep(500).next().value.then(function(){console.log(2222)})
es5、promise、async以及generator中实现sleep的方法_es6 线程休眠_小小小小小亮的博客-CSDN博客
• 介绍一下 promise,及其底层如何实现
Promise 是js中的一个原生对象,保存着未来将要结束的事件,解决回调地狱的问题,她有两个特征:
Promise 构造函数的基本用法
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve('ok!')
} else {
reject('失败了!')
}
}, 1000);
})
promise.then(result => console.log(result))
.catch(error => console.log(error))
最简单代码实现 promise
class PromiseM {
constructor (process) {
this.status = 'pending'
this.msg = ''
process(this.resolve.bind(this), this.reject.bind(this))
return this
}
resolve (val) {
this.status = 'fulfilled'
this.msg = val
}
reject (err) {
this.status = 'rejected'
this.msg = err
}
then (fufilled, reject) {
if(this.status === 'fulfilled') {
fufilled(this.msg)
}
if(this.status === 'rejected') {
reject(this.msg)
}
}
}
//测试代码
var mm=new PromiseM(function(resolve,reject){
resolve('123');
});
mm.then(function(success){
console.log(success);
},function(){
console.log('fail!');
});
一文告诉你什么是回调地狱,如何解决回调地狱?_混子前端的博客-CSDN博客
• 如何解决异步回调地狱
promise、generator、async/await
Generator 生成器
- 函数前添加 *,生成一个生成器
- 一般配合 yield 关键字使用
- 最大特点,惰性执行,调 next 才会往下执行
- 主要用来解决异步回调过深的问题
• promise+Generator+Async 的使用
promise对象和Async异步函数前后文都有,这里讲generator函数
Generator 函数:
1、分段执行,可以暂停
2、可以控制阶段和每个阶段的返回值
3、可以知道是否执行到结尾,value 表示返回值,done 表示遍历是否结束,false没结束,true结束了
{
function* gen() {
yield 'hello'
yield 'world'
return 'ending'
}
let it = gen()
it.next() // {value: "hello", done: false}
it.next() // {value: "world", done: false}
it.next() // {value: "ending", done: true}
it.next() // {value: undefined, done: true}
}
26:promise+Generator+Async 的使用 - 简书
• 简单的实现一个 promise
我们先分析一下promise 都有哪些功能:
1、promise 是一个类,所以 我们可以使用class关键字声明
2、promise 有三种状态 pending (等待执行) fulfilled (执行成功) rejected (执行失败)
- 状态只能有两种转换方式:
· pending --> fulfilled
· pending --> rejected
- 只要状态发生了改变,这一次promise就算执行完成了。
- 不管结果是成功还是失败,都不会再变成另一种。
3、promise 接收的是一个匿名函数,我们称之为执行器
4、执行器函数有两个回调方法,一个叫做 resolve reject。
- 这两个方法是从 promise 内部调用的并且需要传一个结果出来,所以应该是promise类内部的方法。
- resolve 在执行之后,promise的状态就变成了fulfilled
- rejecte 在执行之后,promise的状态就变成了rejected
5、resolve,reject都有一个参数 来表示成功的结果和失败的的原因,我们将这两个值保存在promise内部,因为在then的时候还会使用到他们
6、promise有一个then方法,它有两个参数,一个是成功的回调方法,一个是失败的回调方法,每个方法都能接收到对应状态的返回结果。
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
//在myPromise的原型上定义链式调用的then方法:
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
//上述就是一个初始版本的myPromise,在myPromise里发生状态改变,
//然后在相应的then方法里面根据不同的状态可以执行不同的操作。
var p=new myPromise(function(resolve,reject){
resolve(1)
});
p.then(function(x){
console.log(x) //输出1
})
//但是这里myPromise无法处理异步的resolve.比如:
var p=new myPromise(function(resolve,reject){
setTimeout(function(){
resolve(1)
},1000)
});
p.then(function(x){
console.log(x) //无输出
})
异步函数async/await
异步函数可以将异步代码写成同步的形式,让代码不再有回调函数的嵌套
const fn = async () => {}
async function fn () {}
async
- 默认返回promise对象
- 在异步函数内部使用return返回结果,结果会被包裹在promise对象中,return关键字代替resolve方法
- 在异步函数内部使用throw抛出程序异常
- 链式调用then方法获取执行结果,catch获取错误信息
await
- await只能出现异步函数中
- await后面只能写promise对象,不可以写其他类型的api
- await的作用是暂停异步函数向下执行,直到promise返回结果
异步的本质
async/await是语法糖,异步的本质还是回调函数,代码执行规则依然按照Event Loop进行。
async function async1(){
console.log('async1 start') // 顺序2
// await 的后面,都可以看作是callback里的内容,即异步
await async2()
// Promise.resolve.then(()=>{console.log('async1 end'})
console.log('async1 end') // 顺序5
}
async function async2(){
console.log('async2') // 顺序3
}
console.log('script start') // 顺序1
async1()
console.log('script end') // 顺序4
原型和原型链
作用:被用于复制现有实例来生成新实例的函数,可以很好的实现继承
总结
1、
__proto__:对象拥有的隐式原型(对象原型)
prototype:函数拥有的显式原型(原型对象)
2、
对象只有 __proto__
函数 __proto__ 和 prototype 两者都有
3、总结 __proto__:
- 通常被称作隐式原型,每个对象都拥有该属性。
- 对象的[[prototype]]其实就是__proto__。
4、constructor构造函数
对象原型(proto_)和构造函数(prototype)原型对象里面都有一个属性contructor属性。
- contructor我们称为构造函数,因为她指回构造函数本身。
- contructor主要用于记录该对象引用于哪个构造函数,他可以让原型对象重新指向原来的构造函数。
- 给prototype追加方法,不会覆盖contructor。
- 给prototype赋值,会覆盖掉contructor。
function Person() {}
var person = new Person();
得到三个等式:
Object.getPrototypeOf(person) === Person.prototype
Person===Person.prototype.constructor
person.constructor === Person.prototype.constructor
原型链
查找属性的过程形成的一条线索就叫做原型链,大家可以把原型链拆开来理解:原型和链。
- 原型就是我们的prototype
- 链就是__proto__,它让整个链路连接起来。
想要理解原型链,我们还得理解__proto__指向哪儿,也就是说它指向那个构造函数,比如上面的obj对象的__proto__指向的就是Person构造函数,所以我们继续往Person上查找。
https://www.cnblogs.com/loveyaxin/p/11151586.html
• Function._proto_(getPrototypeOf)是什么?
Function.__proto__==Object.prototype //false
Function.__proto__==Function.prototype//true
我们发现 Function 的原型也是 Function。
-
JS中的函数可以作为函数使用,也可以作为类使用
-
作为类使用的函数实例化时需要使用new
-
为了让函数具有类的功能,函数都具有
prototype
属性。 -
为了让实例化出来的对象能够访问到
prototype
上的属性和方法,实例对象的__proto__
指向了类的prototype
。所以prototype
是函数的属性,不是对象的。对象拥有的是__proto__
,是用来查找prototype
的。 -
prototype.constructor
指向的是构造函数,也就是类函数本身。改变这个指针并不能改变构造函数。 -
对象本身并没有
constructor
属性,你访问到的是原型链上的prototype.constructor
。 -
函数本身也是对象,也具有
__proto__
,他指向的是JS内置对象Function
的原型Function.prototype
。所以你才能调用func.call
,func.apply
这些方法,你调用的其实是Function.prototype.call
和Function.prototype.apply
。 -
prototype
本身也是对象,所以他也有__proto__
,指向了他父级的prototype
。__proto__
和prototype
的这种链式指向构成了JS的原型链。原型链的最终指向是Object
。Object
上面原型链是null,即Object.__proto__ === null
。
• JS 原型链,原型链的顶端是什么?Object 的原型是什么?Object 的原型的原型是什么?在数组原型链上实现删除数组重复数据的方法
- 原型链顶端是Object.prototype
- Object的原型是Object.prototype
- Object的原型的原型是null,即Object.prototype没有原型,Object.prototype.__proto__ 不存在
Object.prototype.__proto__ === null,//返回true
-
构造函数
典型的OOP的语言中,都存在类的概念,类就是对象的模板,对象就是类的实例化;
创建一个构造函数就是创建一个类。
类就是抽离出对象的公共属性和方法。
创建一个构造函数
function Person(name, height) {
this.name = name;
this.height = height;
this.bark = function(x) {
return x
}
}
var boy = new Person('Keith', 180);
console.log(boy); //Person {name: 'Keith', height: 180, bark: ƒ}
console.log(boy.constructor); //f Person(){} //整个构造函数原型
console.log(boy.bark(8)); //8
1、首字母大写,为了和普通函数区分
2、通过new创建多个实例对象
3、里面的属性和方法前必须加this,this就表示当前运行时的对象
4、ECMAScript提供了多个内置构造函数,如 Object、Array、String、Boolean、Number、Date…等等。var obj = new Object(); var arr = new Array();
ECMAScript也允许自定义构造函数
构造函数的原理:
构造函数的弊端:
- 缺点1: 如果构造函数中有很多的方法那么就会开辟很多的空间,浪费内存资源
- 缺点2: 如果在全局情况下声明函数,虽然解决了内存资源浪费的问题,但是又会出现全局变量污染的问题
- 缺点3: 可以重新声明一个对象专门存放这些方法,但是新的问题时,如果有很多个构造函数,就要声明很多个这样的对象
- 总结: 为解决创建构造函数的弊端的问题,我们可以直接使用原型对象
原型对象----构造函数每次在声明的时候,系统会自动的帮我们创建一个与之对应的对象
prototype---- 属于构造函数,指向原型对象
proto----属于构造函数的实例化对象,指向原型对象
constructor----属于原型对象,指向该原型对象与之对应的构造函数
ES6创建类和对象
class Star { // 1、首字母大写,类名不要加()
constructor(name,age){ //无需加function
this.name = name;
this.age = age
}
listen(music){
console.log('姓名:'+this.name+'年龄:'+this.age+'爱听的音乐:'+ music);
}
}
// 2、利用类创建对象 new
var a = new Star('Nan',10); //Star {name: "Nan", age: 10}
var b = new Star('Chen',20);
a.listen('One Day');
console.log(a);
console.log(b);
注意:
1、通过class创建类,类名最好首字母大写
2、constructor(){},用来接收传递的参数,并且返回实例对象
3、constructor(){} 一旦生成new实例对象时,就会自动调用这个函数,如果不写这个函数,其类也会生成这个函数
4、生成实例 new 不能省略
5、语法规范:创建类后的类名不要加小括号 ,生成实例对象的类名需要加(),构造函数无需加function
6、类的方法和constructor函数同级书写,不写在其里面
面向对象的特性
什么是面向对象?
对比来看,
面向过程:按照分析好的步骤解决问题
面向对象:以对象功能来划分问题
三大基本特性:封装,继承,多态
减少前端代码耦合
什么是代码耦合?代码耦合的表现是改了一点毛发而牵动了全身,或者是想要改点东西,需要在一堆代码里面找半天。由于前端需要组织js/css/html,耦合的问题可能会更加明显
• 实现 JS 中所有对象的深度克隆(包装对象,Date 对象,正则对象)
不管是浅拷贝还是深拷贝都用到for in 遍历
1、浅拷贝(内存地址一样,会改变原对象)
(1)手写for in 浅拷贝
(2)Object.assign() 方法
(3)函数库lodash的_.clone方法
(4)使用数组 API,如 concat
或者 slice
以及拓展运算符...
2、深拷贝(开辟新内存,不改变原对象,新旧各自独立)
(1)JSON.parse(JSON.stringify()) (不能处理函数和正则,因为函数会消失,正则变成空)
(2)函数库lodash的_.cloneDeep方法
(3)jQuery.extend()方法
(4)递归函数实现(只能深复制数组和对象)
①简单的深拷贝
function deepClone(obj) {
var newObj = obj instanceof Array ? [] : {}
for (const i in obj) {
newObj[i] = typeof obj[i] == 'object' ? deepClone(obj[i]) : obj[i]
}
return newObj
}
②考虑到包装对象,Date 对象,正则对象的深拷贝
递归方法实现深度克隆原理:
在for in 里调用自身
遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
// 在深拷贝的基础上,对(包装对象Number,String,Boolean;Date 对象,RegExp正则对象)进行深拷贝
function deepClone(obj) {
let newObj = obj instanceof Array ? [] : {}
for (let i in obj) {
// for...in 会遍历原型上的属性,此处只拷贝obj对象自身的属性
if (obj.hasOwnProperty(i)) {
let type = Object.prototype.toString.call(obj[i])
if (typeof obj[i] == 'object') {
// 拷贝的值为对象,则需要深拷贝
if (type == '[object Date]') {
newObj[i] = new Date(obj[i].valueOf())
} else if (type == '[object RegExp]') {
// 正则对象
let pattern = obj[i].valueOf()
let flags = ''
flags += pattern.global ? 'g' : ''
flags += pattern.ignoreCase ? 'i' : ''
flags += pattern.multiline ? 'm' : ''
newObj[i] = new RegExp(pattern.source, flags)
} else if (type == '[object Array]' || type == '[object Object]') {
// 数组或对象
newObj[i] = deepClone(obj[i])
} else {
// 包装对象Number,String,Boolean
newObj[i] = obj[i].valueOf()
}
} else if (typeof obj[i] == 'function') {
// 函数
newObj[i] = new Function('return ' + obj[i].toString())()
} else {
// 拷贝的值为原始值,则直接复制该值
newObj[i] = obj[i]
}
}
}
return newObj
}
前端深拷贝与浅拷贝(附实现方法)_godlike-icy的博客-CSDN博客
实现 js 中所有对象的深拷贝(包装对象,Date 对象,正则对象)_js 复制date对象__L...的博客-CSDN博客
javascript - 如何写出一个惊艳面试官的深拷贝? - code秘密花园 - SegmentFault 思否
包装对象
定义
对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper
)。
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number
、String
、Boolean
三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false
new Map()
Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
1、let mapObj = new Map();
2、let mapObj = new Map([[key,value],[key,value]]); //默认带初始化参数的定义
递归函数
一个函数在内部可以调用自身,就是递归函数
let num = 1;
function fn() {
console.log(num);
if(num == 6) return
num++;
fn();
}
fn();
• 简单实现 Node 的 Events 模块
实例1:
简介:观察者模式或者说订阅模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
node中的Events模块就是通过观察者模式来实现的:
var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');
这样,eventEmitter 发出 say 事件,通过 On 接收,并且输出结果,这就是一个订阅模式的实现,下面我们来简单的实现一个 Events 模块的 EventEmitter。
function Events(){
this.on=function(eventName,callBack){
if(!this.handles){
this.handles={};
}
if(!this.handles[eventName]){
this.handles[eventName]=[];
}
this.handles[eventName].push(callBack);
}
this.emit=function(eventName,obj){
if(this.handles[eventName]){
for(var i=0;o<this.handles[eventName].length;i++){
this.handles[eventName][i](obj);
}
}
}
return this;
}
这样我们就定义了 Events,现在我们可以开始来调用:
var events=new Events();
events.on('say',function(name){
console.log('Hello',nama)
});
events.emit('say','Jony yu');
//结果就是通过 emit 调用之后,输出了 Jony yu
(2)每个对象是独立的
var event1=new Events();
var event2=new Events();
event1.on('say',function(){
console.log('Jony event1');
});
event2.on('say',function(){
console.log('Jony event2');
})
event1.emit('say');
event2.emit('say');
event1、event2 之间的事件监听互相不影响
输出结果为'Jony event1' 'Jony event2'
https://www.cnblogs.com/fangtoushi/p/16013134.html
• 箭头函数中 this 指向举例
var a=11;
function test2(){
this.a=22;
let b=()=>{
console.log(this.a)
}
b();
}
var x=new test2();//输出 22
• JS 判断类型
• 数组常用方法
1、增
- push(),在数组末尾添加,返回数组长度
- unshift(),在数组开头添加,返回数组长度
- splice(),传入三个参数,splice(开始位置,0删除0项,插入的元素),返回空数组
- concat(),创建副本,在副本末尾添加参数,返回此副本,不改变原数组
2、删
前面三种都会改变原数组,最后一项不改变原数组:
- pop(),删除数组最后一项,返回被删除的项
- shift(),删除数组第一项,返回被删除的项
- splice(),传入2个参数,splice(0开始位置,1删除1项),返回包含删除元素的数组
- slice(),从数组中截取指定的字段,返回出来,创建副本,slice(1)等同于slice(1, 1),截取索引为1的项到最后一项,slice(1,4)截取索引为1的项到索引为3的项,不改变原数组
3、改
即修改原来数组的内容,常用splice
- splice(),传入三个参数,splice(开始位置,1删除1项,插入的元素),返回修改后的数组,改变原数组
4、查
即查找元素,返回元素坐标或者元素值
- indexOf(),返回要查找的元素在数组中的索引,如果没找到则返回 -1
- includes(),返回要查找的元素在数组中的索引,找到返回
true
,否则false
- find(),返回第一个匹配的元素
-
findIndex(),找到符合条件的项的下标,并且返回第一个
5、排序方法
数组有两个方法可以用来对元素重新排序:
- reverse(),反转数组
- sort(),接收一个比较函数,判断哪个值应该排在前面
6、转换方法
- join() ,用指定的分隔符,将数组分割成字符串。返回值:返回一个新的数组
- split(),用指定的分隔符,将字符串分割成数组。返回值:返回一个新的数组
7、求和方法
- reduce(),用于数组求和,扁平数组
//其中,前两项参数是必须的
arr.reduce(function(prev,cur,index,arr){
...
}, init);
- prev: 可指定初始值init,不指定默认为数组的第一项arr[0]
- cur: 正在处理的元素,若prev制定了初始值init,则是数组第一项arr[0],否则默认是第二项arr[1]
- index:正在处理的元素的索引。若指定了初始值 init,则起始索引号为 0,否则从索引 1 起始。
- arr:用于遍历的数组。
求和案例:
①数组求和
var arr = [1,5,8,10,15,66,65,25,48,55]
// reduce
var sum = arr.reduce(function(prev,cur){
return prev+cur;
});
console.log(sum); //298
②求最大值
var arr = [1,5,8,10,15,66,65,25,48,55]
var max = arr.reduce(function (prev, cur) {
return Math.max(prev,cur);
});
console.log(max) //66
③扁平一个二维数组
var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];
var res = arr.reduce((prev, cur) => prev.concat(cur), []);
console.log(res) // [1,2,8,3,4,9,5,6,10]
JS中的reduce()函数介绍_reduce函数_潮汐未见潮落的博客-CSDN博客
8、迭代方法 / 批处理方法
常用来遍历数组的方法(都不改变原数组)有如下:
遍历就是把数组中的每个元素从头到尾都访问一次,以下方法都是接收三个参数,例如:
[ ].forEach(function(item, index, arr) { }
- some(),数组中有一项符合条件就返回true
- every(),数组的每一项都符合条件才返回true
- forEach(), 没有返回值
- filter(),有返回值, 过滤出符合条件的元素
- map(),映射,有返回值,返回值是生成的新数组
5、不改变原数组的
concat(),slice(),join() ,split(),filter(),indexOf(),reduce(),遍历数组的所有方法
foreach和map的不同点:
(1)map()方法会得到一个新的数组并返回,forEach()会修改原来的数组。
map遍历的后的数组通常都是生成一个新的数组,新的数组的值发生变化,当前遍历的数组值不会变化。
(2)forEach()允许callback更改原始数组的元素。map()返回新的数组。
forEach遍历通常都是直接引入当前遍历数组的内存地址,生成的数组的值发生变化,当前遍历的数组对应的值也会发生变化。
foreach和map的共同点:
(1)都是循环遍历数组中的每一项。
(2)每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)。
(3)匿名函数中的this都是指向window。
(4)只能遍历数组。
vue使用场景
- forEach() 适合你并不打算改变数据的时候,而只是想用数据做一些事情—比如存入数据库或者打印出来。
- map() 适合你要改变数据值的时候,不仅仅在于它更快,而是返回一个新的数组;这样的优点在于你可以使用复合(composition) (map() , filter() , reduce() 等组合使用)来玩出更多的花样。
• 数组去重
原数组
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
1.对象属性(indexof)
利用对象属性key排除重复项
遍历数组,每次判断新数组中是否存在该属性,不存在就存储在新数组中
并把数组元素作为key,最后返回新数组
这个方法的优点是效率高,缺点是使用了额外空间
var newArr = [];
arr.forEach((key,index)=>{
if(newArr.indexOf(key) === -1){
newArr.push(key)
} })
console.log(newArr);// [1, '1', 17, true, false, 'true', 'a', {}, {}]
2.new Set(数组)
作用:Set() 用来存储键,是键的集合;即用来管理一组键(key)
Set是一系列无序、没有重复值的数据集合,传入一个需要去重的数组,Set会自动删除重复的元素
再将Set转数组返回。此方法效率高,代码清晰,缺点是存在兼容性问题
const newArr = [...new Set(arr)];
console.log(newArr);// [1, '1', 17, true, false, 'true', 'a', {}, {}]
3.new Map()
作用: Map() 用来存储键值对,是键值对的集合;即用来管理键值对(key:value)
利用Map的键值对同名覆盖,再将Map转数组
const m = new Map();
for (let i = 0; i < arr.length; i++) {
m.set(arr[i], arr[i]);
}
const newArr = []
m.forEach(function (value, key) {
newArr .push(value)
})
console.log(newArr );//[1, '1', 17, true, false, 'true', 'a', {}, {}]
4.filter() + indexof
filter把接收的函数依次作用于每一个数组项,然后根据返回值 true or false 决定是否保留该值
优点在于可在去重时插入对元素的操作,可拓展性强
const newArr= arr.filter(function(item,index,self){
return self.indexOf(item) === index;
})
console.log(newArr);// [1, '1', 17, true, false, 'true', 'a', {}, {}]
5.reduce() + includes
reduce()把结果继续和序列的下一个元素做累加计算
利用reduce遍历和传入一个空数组作为去重后的新数组,然后内部判断新数组中是否存在当前遍历的元素,不存在就插入新数组
缺点在于时间消耗多,内存空间也额外占用
const newArray = arr.reduce((newArr, element) => {
if (!newArr.includes(element)) {
newArr.push(element);
}
return newArr;
}, []);
6、Object键值对去重
注意点:
在数据量较低时,以上五个方法无较为明显的区别(10000条)
高于10000条时,前两种方法时间消耗最少,后三种时间消耗依次增加
第一种方法空间占用多,当下很多项目不再考虑低版本游览器兼容性问题
推荐使用Set()去重方法
• 如何让事件先冒泡后捕获
• 说一下事件委托
然后利用冒泡原理影响设置每个子节点。
• 事件委托以及冒泡原理。
- 事件委托就是利用冒泡原理,把事件加到父元素或者祖先元素上,触发来执行效果
- 其优点为:可以显著的提高事件的处理速度,减少内存的占用
- 事件冒泡的原理:事件按照从最特定的目标到最不特定的目标的顺序触发,也就指事件向上传导,当后代元素上的事件被触发时,其祖先的相同事件也会被触发,而事件就是从dom树层层往上传递,直至传递到dom的根节点
• 事件代理在捕获阶段的实际应用
- 事件代理:把一个元素响应事件的函数委托到另一个元素上,也叫事件委托;
- 事件流的三个阶段:1、捕获阶段----->2、目标阶段----->3、冒泡阶段;
- 事件代理(事件委托)主要就是利用了
冒泡
的特点,是在冒泡阶段完成的;
2、好处: 只操作一次DOM,提高性能,减少事件绑定,从而减少内存占用。
3、原理
不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,
然后利用冒泡原理影响设置每个子节点
4、详细解释:
事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素。
当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后再外层元素上去执行函数。
5、在捕获阶段的实际应用
• 去除字符串首尾空格
• 能来讲讲 JS 的语言特性吗
定义:
区别:
HTML:标记语言
CSS:标记语言
JavaScript:动态脚本语言
JavaScript特性:
- 面向对象语言,是基于原型的面向对象;
- 脚本语言(动态语言);
- 事件驱动,,运行在客户端浏览器上;
- 解释性语言,不用预编译,直接解析执行代码;
- 动态性,程序在运行时可以改变其结构;
- 是弱类型语言(&松散类型),较为灵活;
- 单线程与异步处理共存,回调函数事件;
- 与操作系统无关,跨平台的语言。
• 说说 C++,Java,JavaScript 这三种语言的区别
1、从静态类型还是动态类型来看
- 对于静态类型,在编译后会大量利用已知类型的优势,如 int 类型,占用 4 个字节,编译后的代码就可以用内存地址加偏移量的方法存取变量,而地址加偏移量的算法汇编很容易实现。
- 对于动态类型,会当做字符串通通存下来,之后存取就用字符串匹配。
2、从编译型还是解释型来看
- 像 C、C++,需要编译器编译成本地可执行程序后才能运行,由开发人员在编写完成后手动实施。用户只使用这些编译好的本地代码,这些本地代码由系统加载器执行,由操作系统的 CPU 直接执行,无需其他额外的虚拟机等。
- 源代码=》抽象语法树=》中间表示=》本地代码
- 像 JavaScript、Python,开发语言写好后直接将代码交给用户,用户使用脚本解释器将脚本文件解释执行。对于脚本语言,没有开发人员的编译过程,当然,也不绝对。
- 源代码=》抽象语法树=》解释器解释执行。
- 对于 JavaScript,随着 Java 虚拟机 JIT 技术的引入,工作方式也发生了改变。可以将抽象语法树转成中间表示(字节码),再转成本地代码,如 JavaScriptCore,这样可以大大提高执行效率。也可以从抽象语法树直接转成本地代码,如 V8。
- Java 语言,分为两个阶段。首先像 C++语言一样,经过编译器编译。和 C++的不同,C++编译生成本地代码,Java 编译后,生成字节码,字节码与平台无关。第二阶段,由Java 的运行环境也就是 Java 虚拟机运行字节码,使用解释器执行这些代码。一般情况下,Java 虚拟机都引入了 JIT 技术,将字节码转换成本地代码来提高执行效率。
- 注意,在上述情况中,编译器的编译过程没有时间要求,所以编译器可以做大量的代码优化措施。
3、对于 JavaScript 与 Java 它们还有的不同:
- 对于 Java,Java 语言将源代码编译成字节码,这个同执行阶段是分开的。也就是从源代码到抽象语法树到字节码这段时间的长短是无所谓的。
- 对于 JavaScript,这些都是在网页和 JavaScript 文件下载后同执行阶段一起在网页的加载和渲染过程中实施的,所以对于它们的处理时间有严格要求。
• 如何判断一个数组
1、坑 typeof
2、判断方法
(1)instanceof 运算符
let arr = [1,2,3];
console.log(arr instanceof Array); //true
(2)constructor 构造函数
let arr = [1,2,3];
console.log(arr.constructor == Array); //true
(3)isArray
(ES5新增数组方法,判断数组是不是数组)
let arr = [1,2,3];
console.log(Array.isArray(arr)); //true
(4)Object.prototype.toString.call()
(把对象转化成字符串和一个已知的对象对比)
let arr = [1,2,3];
console.log(Object.prototype.toString.call(arr) == '[object Array]'); //true
(5)Array原型链上的isPrototypeOf
(Array.prototype表示Array的构造函数的原型isPrototypeOf()方法可以判断当前对象是否是另一个对象的原型,或者说一个对象是否被包含在另一个对象的原型链中)
let arr = [1,2,3];
console.log(Array.prototype.isPrototypeOf(arr)); //true
(6)Object.getPrototypeOf()
(Object.getPrototypeOf()方法返回指定对象(arr)的原型,然后和Array的原型对比)
let arr = [1,2,3];
console.log(Object.getPrototypeOf(arr) == Array.prototype); //true
https://www.cnblogs.com/srqsl/p/16895063.html
• 你说到 typeof,能不能加一个限制条件达到判断条件
// 1、剔除特殊的null
function TypeOf (obj) {
if (typeof obj === 'object' && obj !== null) {
return 'object';
} else if (obj === null) {
return 'null';
}
return typeof obj;
}
// 2、判断object中有数组
function hasArray (obj) {
if (TypeOf(obj) === 'object') {
for (let i in obj) {
if (obj[i] instanceof Array) return true;
}
}
return false;
}
let arr = [1, 2, 3]
hasArray(arr)
• JS 实现跨域
1、广义的跨域
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。
1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等
2、同源策略
所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送
3、JS 实现跨域
(1)JSONP跨域
原理:script标签引入js文件不受跨域影响。不仅如此,带src属性的标签都不受同源策略的影响。通过动态创建 script,再请求一个带参网址实现跨域通信。
(2)document.domain+ iframe 跨域
此方案仅限主域相同,子域不同的应用场景。两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。
(3)location.hash + iframe 跨域
a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。
(4)window.name + iframe 跨域
通过 iframe 的 src 属性由外域转向本地域,跨域数据即由iframe 的 window.name 从外域传递到本地域。
(5)postMessage 跨域
可以跨域操作的 window 属性之一。
(6) CORS 跨域资源共享
服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带cookie 请求,前后端都需要设置。
(7)nginx代理跨域
启一个代理服务器,实现数据的转发
(8)Web Sockets跨域
web sockets原理:在JS创建了web socket之后,会有一个HTTP请求发送到浏览器以发起连接。取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为web sockt协议
以上,参考nginx - 前端常见跨域解决方案(全) - 个人文章 - SegmentFault 思否
js前端解决跨域的八种实现方案_javascript技巧_脚本之家
JS数据类型
共八种,按照类型来分有原始数据类型和引用数据类型:
栈:原始数据类型:String、Number、Boolean、Null、Undefined、Symbol、BigInt
堆:引用数据类型:Object【Object是个大类,function函数、array数组、date日期...等都归属于Object】
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
Symbol是原始数据类型, 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
// Symbol(字符串) 返回一个具有唯一的标识符,标识符是原始值
const sym1 = Symbol('hello')
const sym2 = Symbol('hello')
console.log(sym1===sym2);//false
// const sym3 = new Symbol('hello')//用new调用Symbol会报错
const sym4 = Symbol()
const sym5 = Symbol()
const obj = {
x:1,
y:2,
[sym4]:'hello',
[sym5]:'你好'
}
console.log(obj[sym4]); //hello
console.log(obj[sym5]); //你好
BigInt 是一种原始数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
//字面量法
const bi1 = 100000n
//内置函数法
const bi2 = BigInt(100000) //100000n
const bi2 = BigInt('100000') //100000n
内置对象与原生对象
内置(Build-in
)对象与原生(Naitve
)对象的区别在于:前者总是在引擎初始化阶段就被创建好的对象,是后者的一个子集;而后者包括了一些在运行过程中动态创建的对象。
原生对象(New后的对象)
内置对象(不需要New)
JS数据类型:null 和 undefined 有什么区别?
Null 只有一个值,是 null。不存在的对象。
Undefined 只有一个值,是undefined。没有初始化。undefined 是从 null 中派生出来的。
简单理解就是:undefined 是没有定义的,null 是定义了但是为空。
JS数据类型:null 不存在的原因是什么?如何解决?
不存在的原因是:
1、方法不存在
2、对象不存在
3、字符串变量不存在
4、接口类型对象没初始化
解决方法:
做判断处理的时候,放在设定值的最前面
• null == undefined 为什么
JS数据类型:== 和 === 有什么区别,什么场景下使用?
JavaScript
中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等,全等运算符不会做类型转换null
和undefined
比较,相等操作符(==)为true
,全等为false
- 除了在比较对象属性为
null
或者undefined
的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===)
• 重排和重绘,讲讲看
重绘(repaint 或 redraw):
当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
触发重绘的条件:改变元素外观属性。如:color,background-color ,opacity等。
注意:table 及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。
重排(重构/回流/reflow):
当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。
重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,
重绘不一定重排,但重排必然会重绘。
触发重排
- 页面初始化、页面第一次渲染时
- DOM树发生变化,增删结点时
- Render树发生变化,比如padding的值发生改变时
- 浏览器窗口的resize发生变化时
- 获取元素的某些属性时
- 字体大小改变
触发重绘
- 触发重排一定能触发重绘,重绘也可以单独被触发
- 背景色、颜色、透明度发生变化
- 字体大小变化不止会触发重绘还会触发重排
如何减少重绘重排次数?
- 用transform做形变和位移可以减少重排
- 避免逐个修改结点样式,尽量一起修改,将逐步渲染转变为整体渲染
- 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性的append到真实的DOM树中进行渲染
- 可以将需要多次修改的DOM元素设置display:none;等其他操作结束后再显示。
display:none;和visibility:hidden;的区别
display:none;会触发重绘和重排,隐藏后不占空间,产生了位置变化
visibility:hidden;只会触发重绘,不会触发重排,隐藏后占据空间,没有产生位置变化
【前端面试题】页面绘制-重绘和重排_页面重绘_二琳爱吃肉的博客-CSDN博客
• 不同数据类型的值的比较,是怎么转换的,有什么规则
链接: 不同数据类型的值的比较,是怎么转换的,有什么规则__牛客网
来源:牛客网
1) 数字和字符串、布尔类型、数组进行比较时,字符串(或布尔类型、或数组)先转换为数字(Number),再进行比较;
2) 字符串和布尔类型比较,进行比较的两个数据同时经过Number()的洗礼后再进行比较。数组和布尔类型的比较也如此。
3) undefined 除了和null进行非全等比较返回true,其它均返回false。null 除了和 undefined进行非全等比较返回true,其它均返回false。
null==undefined //true
null===undefined //false
null 和 undefined 和其它值永远不相等
NaN 和其它值永不相等
4) 数组(或对象)和字符串进行比较时,数组(或对象)会转换成字符串再进行比较。
• 有一个游戏叫做 Flappy Bird,就是一只小鸟在飞,前面是无尽的沙漠,上下不断有钢管生成,你要躲避钢管。然后小明在玩这个游戏时候老是卡顿甚至崩溃,说出原因(3-5 个)以及解决办法(3-5 个)
• 编写代码,满足以下条件:
(1)Hero("37er");
执行结果为 Hi! This is 37er
(2)Hero("37er").kill(1).recover(30);
执行结果为 Hi! This is 37er Kill 1 bug Recover 30 bloods
(3) Hero("37er").sleep(10).kill(2)
执行结果为 Hi! This is 37er
//等 待 10s 后 Kill 2 bugs //注意为 bugs (双斜线后的为提示信息,不需要打印)
参考答案:
function Hero(name){
let o=new Object();
o.name=name;
o.time=0;
console.log("Hi! This is "+o.name);
o.kill=function(bugs) {
if(bugs==1){
console.log("Kill "+(bugs)+" bug");
}else {
setTimeout(function () {
console.log("Kill " + (bugs) + " bugs");
}, 1000 * this.time);
}
return o;
};
o.recover=function (bloods) {
console.log("Recover "+(bloods)+" bloods");
return o;
}
o.sleep=function (sleepTime) {
o.time=sleepTime;
return o;
}
return o;
}
Hero("37er");
Hero("37er").kill(1).recover(30);
Hero("37er").sleep(10).kill(2);
• 什么是按需加载
• 说一下什么是 virtual dom
Virtual DOM——虚拟节点
1、以前更新ui的方法——遍历查询 dom 树的方式
虚拟 dom 是相对于浏览器所渲染出来的真实 dom 的,在react,vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom 然后修改样式行为或者结构,来达到更新 ui 的目的。
这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示 dom 树,那么每次 dom 的更改就变成了 js 对象的属性的更改,这样一来就能查找 js 对象的属性变化要比查询 dom 树的性能开销小。
2、现在更新ui的方法——前端框架中Virtual DOM的方式
Virtual DOM是根据dom结构生成对应的js对象,Js对象结构就是虚拟dom树。当有节点元素发生改变时,虚拟dom会先跟着变。新旧dom树对比,记录差异,把记录的差异更新到真正的dom'树中。
本质:虚拟dom树就是在js和dom之间做了一个缓存。
Vue 的 Virtual DOM 解决了什么问题
Vue 的 Virtual DOM 解决了以下问题:
提高渲染性能:Vue 通过在内存中创建虚拟 DOM 树,将修改操作批量处理并最小化 DOM 操作次数,从而减少了浏览器重排和重绘的开销,提高了性能。
简化代码复杂度:Vue 的 Virtual DOM 抽象了底层 DOM 的实现细节,让开发者可以更专注于业务逻辑和数据层面的编写和管理,从而简化了代码复杂度。
支持跨平台开发:由于 Virtual DOM 是一个独立于浏览器的抽象层,因此 Vue 可以通过不同的渲染引擎(如浏览器、Node.js、Weex 等)来渲染组件,支持跨多个平台的开发。
virtual Dom 全面理解虚拟Dom_虚拟dom解决了什么问题_叶落风尘的博客-CSDN博客
• 写一个函数,第一秒打印 1,第二秒打印 2
两个方法,第一个是用 let 块级作用域
for(let i=0;i<5;i++){
setTimeout(function(){
console.log(i)
},1000*i)
}
第二个方法闭包+立即执行函数
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i)
},1000*i)
})(i)
}
• 写个函数,可以转化短横线命名到驼峰命名
一、短横线命名到驼峰命名
方法1:采用数组的方法
- 通过split()方法 将字符串转化为一个数组
- toUpperCase()转化成大写
- slice()只有一个参数的时候,则会截取至数组的最后一个单元
- join()把数组中的所有元素放入一个字符串
function getCamelCase1(str) {
var arr=str.split('-') // arr = ['user', 'name']
return arr.map(function(item,index){
if(index===0){
return item
}else{
return item[0].toUpperCase()+item.slice(1)
}
}).join('') //把arr中的元素放入字符串
}
console.log( getCamelCase1( 'user-name' ) ); //userName
方法2:采用正则表达式
1、replace(regxp/substr, replacement),用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串
2、/表达式/[switch]
g:全局匹配
i:忽略大小写
gi:全局匹配+忽略大小写
/-([a-z])/g,匹配以-开头小写a-z任意字母结尾的正则,以下例子意思是将"-n"替换成"-N"
function getCamelCase2(str) {
return str.replace(/-([a-z])/g,function(keb,item){
return item.toUpperCase();
} )
}
console.log( getCamelCase2( 'user-name' ) ); //userName
二、驼峰命名到短横线命名
function getKebabCase1(str){
var arr=str.split('') // arr = ['user', 'Name']
str=arr.map(function(item){
if(item.toUpperCase()===item){ //找到"N"的位置
return '-'+item.toLowerCase();
}else{
return item;
}
})
return str.join( '' )
}
console.log(getKebabCase1('userName')) //user-name
function getKebabCase2(str){
return str.replace(/[A-Z]/g, function(item) {
return '-'+item.toLowerCase()
})
}
console.log( getKebabCase2( 'userName' ) ) //user-name
• JS 中 string 的 startwith 和 indexof 两种方法的区别
startwith 函数
str.startsWith(searchString[, position]),返回布尔值,用于检测字符串是否以指定的前缀开始。
let str2 = "hello world";
let result = str2.startsWith("hello");
let result1 = str2.startsWith("h", 1);
let result2 = str2.indexOf("h");
console.log(result); // true
console.log(result1); // false
console.log(result2); // 0
Indexof 函数
• JS 字符串转数字的方法
parseInt("1234blue"); // 1234
parseInt("0xA"); // 10
parseInt("22.5"); // 22
parseInt("blue"); // NaN
• 有了解过事件模型吗,DOM0 级和 DOM2 级有什么区别,DOM 的分级是什么
首先,W3C DOM标准(DOM分级)先后有三个版本:
1级DOM、2级DOM、3级DOM
DOM事件模型有三个:
0级DOM中定义的DOM0级模型、2级DOM定义的DOM2级模型、IE事件模型
1. 0级DOM
0级DOM包含了一些基本事件和 “原始事件模型” ,在该模型中,事件不会传播,即没有事件流。这种方式所有浏览器都兼容,但是逻辑与显示并没有分离。
2. 1级DOM
前面已经说了,这个标准中,并没有定义出新的事件模型,仅仅是定义了HTML和XML文档的底层结构,不是我们讨论的重点,直接跳过。
3. 2级DOM
在这个标准中,添加了许多新的事件,定义了2级DOM事件模型,该模型指明了新的添加、移除事件监听的方式,还增添了事件流。
2级DOM事件模型将事件传播分为了捕获阶段,目标阶段,冒泡阶段三个阶段。
4. 3级DOM
再次增加了一些事件,提升交互能力。
IE事件模型
在该模型中,事件的传播没有捕获阶段,只有命中与冒泡阶段。
注意!在DOM事件模型中,事件类型没有on,而在IE事件模型中,事件类型需要加上on!
事件流
- 捕获阶段:事件由最外层document向内层传播,一直传递到对应触发事件的元素
- 命中事件:执行对应监听回调
- 冒泡阶段:从命中元素向外层元素传播,一直传递到document
DOM 标准事件流的触发的先后顺序
事件委托中addEventListener
addEventListener(事件名称,执行函数,事件类型)
第三个参数不写或者写false,表示默认使用事件冒泡,写true表示使用事件捕获
element.addEventListener("mousedown", func); //事件冒泡
element.addEventListener("mousedown", func, true); //事件捕获
第三个参数可以是对象类型或布尔值:
element.addEventListener("mousedown", func, { passive: true });
element.addEventListener("mousedown", func, { capture: false });
-
object类型(options)包括三个布尔值选项:
- capture: 默认值为false(即 使用事件冒泡).,true---使用事件捕获;
- once: 默认值为false,是否只调用一次,true---会在调用后自动销毁listener
- passive:不同浏览器默认值不同。true---listener永远不远调用preventDefault方法。根据规范,默认值为false. 但是chrome, Firefox等浏览器为了保证滚动时的性能,在Window,、Document、 Document.body上针对 touchstart 和 touchmove 事件将passive默认值改为了true, 保证了在页面滚动时不会因为自定义事件中调用了preventDefault而阻塞页面渲染。
-
bool类型(useCapture): 默认值为false(即 使用事件冒泡),与capture用法相同。
中参数 capture 可以指定该监听是添加在事件捕获阶段还是事件冒泡阶段,为 false 是事件冒泡,为 true 是事件捕获,并非所有的事件都支持冒泡,比如 focus,blur 等等,我们可以通过 event.bubbles 来判断
- event.stopPropagation:阻止捕获和冒泡阶段中,当前事件的进一步传播,
- event.stopImmediatePropagetion,阻止调用相同事件的其他侦听器,
- event.preventDefault,取消该事件(假如事件是可取消的)而不停止事件的进一步传播。
target 和currentTarget 和 this
事件代理的核心,一是事件交给父元素处理的思想,还有一点就是对 e.target 的使用。
e.target 与 e.currentTarget 的区别:
e.target:返回真正的触发了事件的标签对象。这个会随着我们点击位置的不同,返回不同的标签对象,这个就和事件流有关
e.currentTarget:currentTarget = this,返回绑定事件的标签对象。显然这个是不会变化的,只要事件绑定好了,自然返回的就总是同一个标签
• 写一个 newBind 函数,完成 bind 的功能
Function.prototype是什么?
Function.prototype位于所有函数的原型链上,Function.prototype又通过_proto_指向Object.prototype,所以所有的函数既是Function的实例又是Object的实例,并分别从这两个内置对象继承了很多属性和方法。
bind()的原生实现分步解析
1:通过call,把类数组转换成一个真正的数组,让this能够指向arguments
call() 方法的第二个参数表示传递给slice的参数即截取数组的起始位置
以下两种结果是一样的
Array.prototype.slice.call(arguments)是用来将参数由类数组转换为真正的数组;
[].slice.call(arguments)是用来将参数由类数组转换为真正的数组;
slice()截取数组,
没参数默认从key0到最后一项,浅拷贝,不影响原数组
Array.prototype.slice.call(arguments,1)就是能够将具有length属性(这一点需要注意,必须包含length属性)的对象转换为数组,并使用数组的slice方法
Function.prototype.myBind = function () {
if (typeof this !== 'function') throw 'caller must be a function'
let self = this // 这里是关键 用来和new出来的比较原型来判断是否为new出来的 当前函数对象
let context = arguments[0]
let args = Array.prototype.slice.call(arguments, 1) // 旧:参数
let fn = function () {
let fnArgs = Array.prototype.slice.call(arguments) // 新:参数
// bind 函数的参数 + 延迟函数的参数
// 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
self.apply(this instanceof self ? this : context, args.concat(fnArgs))
}
fn.prototype = Object.create(self.prototype) // 维护原型
return fn
}
js中bind()使用详情_js bind_曾皙的博客-CSDN博客
Array.prototype.slice.call(arguments,1)_Jqy_111的博客-CSDN博客
• 自己实现一个 bind 函数
Function.prototype.bind=function(obj,arg){
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
return function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
}
(2) 考虑到原型链
Function.prototype.bind=function(obj,arg){
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
var bound=function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
var F=function(){}
//这里需要一个寄生组合继承
F.prototype=context.prototype;
bound.prototype=new F();
return bound;
}
• 改变函数内部 this 指针的指向函数(bind,apply,call 的区别)
参考回答:
• call 和 apply 是用来做什么?
• this 的指向 哪几种
- 默认绑定:全局环境中,this 默认绑定到 window。
- 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this 隐式绑定到该直接对象。
- 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到 window。
- 显式绑定:通过 call()、apply()、bind()方法把对象绑定到 this 上,叫做显式绑定。
- new 绑定:如果函数或者方法调用之前带有关键字 new,它就构成构造函数调用。对于this 绑定来说,称为 new 绑定。
- 【1】构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
- 【2】如果构造函数使用 return 语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
- 【3】如果构造函数显式地使用 return 语句返回一个对象,那么调用表达式的值就是这个对象。
• 怎么获得对象上的属性
1、只能获取可枚举属性的:
for...in循环:只遍历对象自身的和继承的可枚举的属性。
const obj1 = {name: 'nick', age: 13, sex: '男'}
for (const key in obj1) {
if (Object.hasOwnProperty.call(obj1, key)) {
const element = obj1[key];
console.log(element); //nick 13 男
}
}
Object.keys():返回对象自身的所有可枚举的属性的键名。
const obj1 = { name: 'nick', age: 13, sex: '男' };
Object.keys(obj1); // ['name', 'age', 'sex']
JSON.stringify():只串行化对象自身的可枚举的属性。
const obj1 = { name: 'nick', age: 13, sex: '男' };
JSON.stringify(obj1) // '{"name":"nick","age":13,"sex":"男"}'
Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
const obj1 = { name: 'nick', age: 13, sex: '男' };
Object.assign(obj1) // {name: 'nick', age: 13, sex: '男'}
Object.values():返回 对象 自身及其原型链上可枚举的属性值的数组
2、包括可枚举和不可枚举的属性
Object.getOwnPropertyNames:主要用于返回对象的自有属性
const obj1 = { name: 'nick', age: 13, sex: '男' };
Object.getOwnPropertyNames(obj1) // ['name', 'age', 'sex']
Reflect.ownKeys():返回自身所有属性键名,包括不可枚举类型,不包括继承的属性
3、只包括不可枚举属性
Object.getOwnPropertySymbols():获取指定对象的所有Symbol属性名
JS获取对象属性API汇总(可/不可枚举、symbol)(建议收藏)_js 如何读取对象中不可枚举的属性_码上十七的博客-CSDN博客
-
枚举属性的作用
什么是枚举?枚举是指对象中的属性是否可以遍历出来,再简单点说就是属性是否可以被列举出来。
枚举属性主要会影响几个方法
ES5中:
for...in //只遍历对象自身的和继承的可枚举的属性
Object.keys() //返回对象自身的所有可枚举的属性的键名
JSON.stringify //JSON.stringify() 方法用于将 JavaScript 值转换为 JSON 字符串。
ES6中:
Object.assign() //会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
可以看出来这些都是可以遍历对象的方法,而这四个操作中只有for...in中会返回继承的属性
• 给出以下代码,输出的结果是什么?原因?
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i); // 5 5 5 5 5
},1000);
}
console.log(i) //5
因为在主程序中遇到异步事件的时候,会将异步事件放到事件队列里面去,等到主程序执行完之后,在去执行事件队列里面的事件,此时主程序已经执行完,i变为5,由于每次输出都是i的值,所以是5个5。
• 给两个构造函数 A 和 B,如何实现 A 继承 B?
寄生组合式继承
function Foo(name) {
this.name = name;
}
Foo.prototype.age = function() {
console.log('父类:' + this.name);
};
Foo.prototype.obj = function() {
console.log('hello world');
};
function fn (name) {
Foo.call(this,name);
}
fn.prototype = Object.create(Foo.prototype);
fn.prototype.constructor = fn;
fn.prototype.age = function() {
console.log('子类:' + this.name);
};
fn.prototype.set = function() {
console.log('set si es5');
};
var a = new fn('lisi');
a.age();
a.obj();
a.set();
https://www.cnblogs.com/boses/p/9588088.html
// promise
A.then(B).then(C).catch(...)
// async/await
(async ()=>{
await a();
await b();
await c();
})()
• 知道 private 和 public 吗
• 基础的 js
Function.prototype.a = 1;
Object.prototype.b = 2;
function A() {}
var a = new A();
console.log(a.a, a.b); // undefined, 2
console.log(A.a, A.b); // 1, 2
• JS 加载过程阻塞,解决方法。
<script src="" async>
<script src="" defer>
指定 script 标签的 async 属性。
async:脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
defer:脚本将在页面完成解析时执行
• JavaScript 中的轮播实现原理?假如一个页面上有两个轮播,你会怎么实现?
• 怎么实现一个计算一年中有多少周?
• 箭头函数和 function 有什么区别
function fnnn () {
console.log(this); // {name: 'chel'}
return () => {
console.log(this); // {name: 'chel'}
}
}
const aaa = { name: 'chel' }
const resFn = fnnn.call(aaa)
resFn()
箭头函数面试题
let objss = {
age: 20,
say: () => {
console.log(this.age) // undefined
}
}
objss.say()
输出结果是undefined,因为此时的this是window