1、JavaScript 的数据类型有哪些?
-
基础数据类型
undefined null number string boolean Symbol
-
引用数据类型 function object array Date RegExp Symbol是表示一个独一无二的变量,就防止我们的变量命名冲突。
判断数组的类型有哪些?
- Array.isArray() 判断
- instanceof 判断: 检验构造函数的prototype属性是否出现在对象的原型链中,返回一个布尔值。
let a = []; a instanceof Array; //true - constructor判断: 实例的构造函数属性constructor指向构造函数
let a = [1,3,4]; a.constructor === Array;//true - Object.prototype.toString.call() 判断
let a = [1,2,3]; Object.prototype.toString.call(a) === '[object Array]';//true
2、原型和原型链
原型
- js 在创建对象的时候,都有一个
__proto__内置属性,用于指向创建它的构造函数的原型对象。 - 每个对象都会有
__proto__,但是只有函数对象才会有 prototype 属性。 - 创建一个
function Person()let children = new Person(),children有一个__proto__属性,他的构造函数是Person,构造函数的原型对象是Person.prototype,children.__proto__指向 Person.prototype - 所有函数对象的
__proto__都指向Function.prototype- 构造函数自身的
__pro__的答案和Object.__proto__的答案是什么Function.prototype Object.__prototype的答案呢?Function.prototype.__proto__和Function.__proto.__.__proto__
- 构造函数自身的
// 原型
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.prototype === Function.__proto__.__proto__); // true
console.log(Object.prototype === Function.prototype.__proto__); // true
- 构造函数的
this永远指向他的实例对象 原型链 - 构造函数他自身也可能是另外一个函数的实例,所以说通过 proto,prototype,这样链接的方式就形成了原型链
- 原型链的尽头是
Object.prototype.__proto__ === null // true
console.log(children.__proto__ === Person.prototype) // true
console.log(String.__proto__ === Function.prototype) // true
console.log(String.construtor === Function) // true
鲨鱼哥出的题
Object.prototype.__proto__` 原型链的尽头是什么? null
console.log(Function.prototype.__proto__ === Function.__proto__.__proto__);
Function.prototype.__proto__ 答案是什么 Function.__proto__.__proto__
构造函数自身的 `__proto__` 是什么?`Object.prototype
console.log(Object instanceof Function) // true
console.log(Function.prototype === Function.__proto__) //true
// 手写代码合集
console.log(Object.__proto__ === Function.prototype); //true
console.log(Function.__proto__.__proto__ === Object.prototype); //true
console.log(Function.prototype.__proto__ === Object.prototype); //true
console.log(Function.__proto__ === Function.prototype); //true
console.log(Object.prototype.__proto__); //null 原型链的尽头
3、JS 继承的几种方式?
// 原型链继承
function Person() {
this.name = "Zaxlct"; //Person.prototype.name = "Zaxlct";
}
// 一样的写法
Person.prototype.sayName = function () {
alert(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = 666;
console.log(person1.name); //666
console.log(person2.name); //Zaxlct
// 原型继承:将子类的原型对象指向父类的实例
// 优点:继承了父类的模板和父类的原型对象
// 缺点:无法实现多继承,无法传参。
function Parent() {
this.name = "小三";
}
function Child() {
this.age = 21;
}
Child.prototype = new Parent();
let obj1 = new Child();
let obj2 = new Parent();
obj1.name = "小舞";
console.log(obj1); // Child {age: 21, name: '小舞'}
console.log(obj2); // Parent {name: '小三'}
// 构造函数继承:在子类构造函数内用 apply、call 来改变父类构造函数的 this 指向。
// 优点:可以实现多继承,可以继承了父类的实例的属性和方法,也可以传参。
// 缺点: 不能继承父类的原型属性和方法。
function Parent(name, age) {
this.name = name;
this.age = age;
this.aaa = 666;
}
function Child(name, age) {
Parent.call(this, name, age);
}
let obj = new Child("小三", 21);
console.log(obj);
console.log(obj.name, obj.age, obj.aaa); // 小三 21 666
// 组合继承:将原型链继承和构造函数函数继承组合在一起。
// 原型链继承是为了保证子类能够继承父类的原型属性和方法
// 构造函数继承是为了保证子类能够继承父类的实例实行和方法
function Parent(name) {
this.name = name;
this.color = [1, 2, 3];
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child("小三", 21);
var child2 = new Child("小舞", 22);
child1.color.push(4);
console.log(child1); // Child {name: '小三', color: Array(4), age: 21}
console.log(child2); // Child {name: '小三', color: Array(3), age: 21}
Object.setPrototypeOf 与 Object.create区别?
setPrototypeOf 与 Object.create区别
使用Object.create,Animal.prototype将会指向一个空对象,空对象的原型属性指向Plants的prototytpe。所以我们不能再访问Animal的原有prototypoe中的属性。Object.create的使用方式也凸显了直接重新赋值。
使用Object.setPrototypeOf则会将Animal.prototype将会指向Animal原有的prototype,然后这个prototype的prototype再指向Plants的prototytpe。所以我们优先访问的Animal,然后再是plants。
在进行俩个原型之间的委托时使用setPrototype更好,Object.create更适和直接对一个无原生原型的对象快速进行委托。
总结1、 一个创造对象,一个修改原型
2、Object.create 是创建参数为
__proto__的对象,Object.setPrototype 是以修改参数为__proto__的对象
4、typeof 和instanceof 的区别
typeof是判断数据类型的,会返回变量的基本类型。instanceof是检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,返回的是布尔值。- 可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型。
Object === Function 通过 Object.__proto__ === Function.prototype 判断Function === Object 通过 Function.prototype.__proto__ === Object.__proto__ 判断
5、常见的 dom 操作有哪些?
- 创建节点:
createElement - 查询节点
getElementByIdquerySelectAllgetElementsByClassName - 更新节点:
innerHTML - 添加节点:
innerHMTL appenChild - 删除节点:
removeChild
<!-- getElementsByClassName -->
<div class="example">1</div>
<div class="example">2</div>
<button onclick="myFunction()">点我</button>
<script>
function myFunction() {
var x = document.getElementsByClassName("example");
x[0].innerHTML = "Hello World!";
}
</script>
<!-- querySelectorAll -->
<h2 class="example">1</h2>
<p class="example">2</p>
<button onclick="myFunction()">点我</button>
<script>
function myFunction() {
var x = document.querySelectorAll(".example");
x[0].style.backgroundColor = "red";
}
</script>
//getElementsByTagName
<script>
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item n</li>
</ul>
// 获取目标元素
const lis = document.getElementsByTagName("li")
// 循环遍历绑定事件
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function(e){
console.log(e.target.innerHTML)
}
}
</script>
6、说一下闭包吧?
- 闭包就是函数内嵌套函数,或者子函数在外调用,子函数在父函数的作用域内不会被释放。
- 内部的函数可以访问外部函数的参数和变量
- 闭包实现了函数的封装缓存,但是消耗内存,可以在退出函数前,将不必要的参数变量删除。
- 应用场景有
防抖和节流,函数柯里化
-
函数柯里化:通过函数调用,继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
优点:
参数复用提高实用性
function add () {
const args = [...arguments]
function fn () {
args.push(...arguments)
return fn
}
fn.toString = function () {
return args.reduce((sum, i) => {
return sm + i
})
}
return fn
}
add(1)(2)(3)(4) // 10
7、防抖和节流
节流:在 N 秒内只执行一次的任务,如果在 N 秒内被重复重发,只执行一次。防抖:在 N 秒后执行的任务,如果在 N 秒内被重复重发,则重新计时。
// 防抖
function debounce(fn, delay = 300) {
//默认300毫秒
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {
console.log(111);
}, 1000)
);
// 节流
// 设置一个标志
function throttle(fn, delay) {
let flag = true;
return () => {
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn();
flag = true;
}, delay);
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
8、 Event Loop 事件循环
事件循环 是为了解决单线程阻塞问题的,<Script> 代码分为同步任务和异步任务,异步任务又分为宏任务和微任务。
- 同步任务指的是:在主线程上排队执行的任务,只有前一个任务执行结束,后一个任务才开始执行。
- 异步任务指的是:不进入主线程,而进入任务队列的任务、只有等主线程任务执行完毕,任务队列的任务才会进入主线程执行。
- 当某个宏任务执行完,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务;如果没有,就会读取宏任务队列中排在最前的任务;
- 执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,以此类推就是事件循环。
- 同步任务(Promise)>异步任务-微任务(Promise.then Promise.catch) > 异步任务-宏任务(settimeout)
9、事件模型、事件绑定、事件监听、事件捕获、事件冒泡、事件委托
事件流分为三个阶段:事件捕获阶段、目标阶段、事件冒泡阶段
- 事件捕获阶段:从最外层 window 向内查找目标元素,查找的过程中
不会处理响应元素注册的冒泡事件 - 目标阶段:
触发事件最底层的元素 - 冒泡阶段:
触发事件从最底层开始向外一层一层的传递到 window 层
在冒泡中,内部元素先被触发,然后再触发外部元素 捕获中,外部元素先被触发,在触发内部元素
原生事件绑定是通过 document.getElementById('btn1') 获取到节点,然后通过 addEventListener 完成事件绑定。
event.stopPropagation() // 阻止冒泡
event.stopImmediatePropagation() // 阻止捕获
event.preventDefault() // 阻止发生默认行为
应用场景是
事件委托
事件委托的原理:就是不给每个子节点单独设置事件监听器,而是设置在父节点上,然后利用事件冒泡原理设置每个子节点。优点:减少了内存消耗和 dom 操作,提高性能,防止重排和重绘,还有就是动态绑定事件,事件绑定在父节点上,新增的元素也能触发同样的事件。- 事件委托的事件有:
clickmousedownmouseupkeydownkeyup focus()blur()没有事件冒泡机制,无法进行事件委托。mousemovemouseout需要计算定位,消耗比较高,也不适合事件委托 如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件
<body>
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item 4</li>
</ul>
<script>
// var a = document.getElementById('a')
// var b = document.getElementById('b')
// var c = document.getElementById('c')
// document.getElementsByClassName
// document.getElementsByName
//注册捕获事件监听器
// a.addEventListener('click', () => { console.log("冒泡a") })
// b.addEventListener('click', () => { console.log('冒泡b') })
// c.addEventListener('click', () => { console.log("捕获c") }, true)
// c.addEventListener('click', () => { console.log("冒泡c") })
// a.addEventListener('click', () => { console.log("捕获a") }, true)
// b.addEventListener('click', () => { console.log('捕获b') }, true)
let list = document.getElementById('list')
list.addEventListener('click', function (e) {
if (e.target.innerHTML === 'item 1') {
console.log('阻止事件冒泡了');
e.stopPropagation()
} else {
console.log(e.target.innerHTML);
}
}, false)
</script>
</body>
10、箭头函数和普通函数的区别?
- 箭头函数的语法比普通函数的
语法更加接简洁,但是不能使用argument、super、new.targer,不能用做构造函数,因为箭头函数没有prototype - 箭头函数的 this 指向它的外层环境,外层对象。而且是在创建时判断的,不是在运行时判断的
- 普通函数的 this 指向调用它的对象
普通函数可以调用 apply call bind 三者改变 this 的指向
最搞笑的一点就是曾经面试的时候背问过
- 箭头函数可不可以调用
applycall呢? 自己大声的回答:不可以丢脸丢到家了其实是可以的,因为 apply call shi ES5 的属性 - 面试官又接着问 会改变 this 的指向吗? 我的回答
可以,哎,对自己无语了其实是不可以的
11、谈谈 this 的理解
函数的调用方式决定了 this 的值
- 函数调用:非严格模式下 this 是指向全局对象的,严格模式下是 undefined
- 方法调用:this 指向调用函数的对象
- 构造函数调用:this 指向 new 创建的实例对象
- call、apply :是指第一个参数 this 的指向、
12、bind apply call 三者的区别?
JavaScript 中 call()、apply()、bind() 的用法
- 三者都能够改变 this 的指向。
- 三者的第一个参数都是 this 指向的那个对象,如果
空或者是undefined、null则默认指向window - apply 传的数数组,call,bind 传的是参数列表
- bind 是返回一个绑定 this 之后的函数,稍后执行。 apply call 是立即执行。
- bind 返回的新函数如果用做构造函数创建新对象,此时 this 不再指向 bind 的第一参数,而是指向 new 创建的实例。
13、this 的指向
14、 new 操作符都干了什么?
- new 创建了一个 obj
- 将 obj 和构造函数通过原型链连接在一起,obj 可以访问构造函数和原型链上的属性和方法
- 将构造函数的 this 绑定在 obj 上
- 返回一个对象,该函数没有返回对象的话,则返回 this
- 构造函数的 this 永远指向它的实例对象
new 操作符新建了一个空对象,这个对象原型指向构造函数的 prototype,执行构造函数后返回这个对象。
// 手写 new
function mynew(fn, ...args) {
let obj = Object.create(fn.prototype)
let res = fn.call(obj, ...args)
if (res && (typeof res === 'object' || typeof res === 'function')) {
return res
}
return obj
}
// 使用
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.play = function () {
console.log(this.age);
}
let p1 = mynew(Person, '小王', 21)
console.log(p1); // {name: "小王", age: 21}
console.log(p1.name); //小王
p1.play() // 21
1. new操作符的实现原理
new操作符的执行过程:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
15、说一下深拷贝和浅拷贝
浅拷贝:以赋值的方式拷贝一个新对象,仍指向同一个地址,修改时原对象也会随之改变。
有三种方法:赋值的方式 =、Object.assign、解构赋值 ...
// 赋值的方式 =
function depp(obj) {
let newobj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newobj[key] = obj[key];
}
}
return newobj;
}
let obj1 = deep(obj);
// Object.assign
let obj1 = Object.assign({}, obj);
// 解构赋值的方式
let obj1 = [...obj];
深拷贝:完全拷贝一个新对象,修改时原对象不会发生改变。
有两种方法进行拷贝: JSON.parse(JSON.stringify(obj))、递归循环
// 深拷贝:新拷贝一个对象,修改时原对象不会改变
// 1. JSON.parse(JSON.stringify(Obj))
// 2. 递归循环拷贝
//
let obj = {
name: "小三",
unfind: undefined,
fun: function () {},
symbol: Symbol("唯一值"),
};
//
let obj1 = JSON.parse(JSON.stringify(obj));
// 缺点就是 undfined,Symbol,function不会进行拷贝,所以要用递归循环
//
function deepOjb(obj, hash = new WeakMap()) {
if (obj === null) return obj; //如果是 null undefined 的话就不拷贝
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj !== "object") return obj; //如果是普通类型的值的话就不进行拷贝
if (hash.get(obj)) return hash.get(obj);
let newobj = new obj.constructor(); //把拷贝对象的构造函数指向此创建的对象
hash.set(obj, newobj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 进行递归拷贝
newobj[key] = deepObj(obj[key], hash);
}
}
}
16、 0.1 + 0.2 为什么不等于 0.3
因为 JS 有一个存储浮点数精度丢失的问题,计算机计算是把十进制转换成二进制计算的,0.1 和 0.2 转换成二进制后有很多位数。计算结果是 0.30000000000000004
可以通过 (0.1 * 10 + 0.2 * 10) / 10 === 0.3
17、作用域和作用域链
作用域有:全局作用域,函数作用域,块级作用域。块级作用域是 ES6 新增的。
- 全局作用域:在
<script>包裹区域内,也是最外层函数包裹区域内。全局变量过多会引起命名冲突。 - 函数作用域:是在函数内部,内层的作用域可以访问外层的作用域,反之不行。
- 块级作用域:就是 let const 定义的区域内,有 {} 包裹着的
作用域链:查找变量的时候会从当前上下文查找变量对象,没有找到,就去父级变量对象上查找,一直找到全局变量对象。由多个上下文变量对象构成的链表就是作用域链。
18、 == === Object.is 的区别
- == 是值相等类型不等的话,他会自动帮你做类型转换,然后输出 true。
- === 是类型和值都必须相等才会输出 true ,不会帮你做类型转换。
- Object 唯一的区别就是除了 NaN 和 -0 和 +0
consolo({} === {}) // false
consolo([] === []) // false
NaN === NaN //false
-0 === +0 //true
Object.is(NaN === NaN) //true
Object.is(-0 === +0) //false
let a = [1]
let b = {}
console.log(typeof b); // object
console.log(typeof null); // object
console.log(typeof a); // object
19、map 和 forEach 的区别?
- map 会返回全新的数组,forEach 不会,返回一个 undefined
- 需要创建新数组的时候使用 map,不需要创建使用
for...in 和 for...of 的区别
for...in:可以遍历对象,数组,输出的是 key
for...of:不能遍历对象,输出的是 value
- for…of 是 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for…in 的区别如下
- for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
- for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链; 对于数组的遍历
- for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性)
- for…of 只返回数组的下标对应的属性值;
总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
怎么判断一个对象是否为空?
Object.keys(obj).length === 0 // true 则为空对象
杂项
typeof NaN 的结果是什么?指的不是一个数字
console.log(typeof NaN); // number
console.log(NaN !== NaN); // true
== 的转换规则 string 转换成 number boolean 转换成 number
|| 和 && 的规则 || 为 true 的话返回第一个,false 的话返回第二个 && 为 true 的话返回第二个,false 的话返回第一个
console.log(0 || 2) // 2
console.log(1 || 2) // 1
console.log(1 && 2) // 2
undefined 和 null 的区别?
undefined 表示未定义的值
- 声明了一个变量,但是没有被赋值
- 访问对象上不存在的属性或者未定义的变量
- 函数定义了形参,没有传实参
null 表示空值