(1)typeof 和 instanceof
1、typeof 对于基本数据类型(boolean、null、undefined、number、string、symbol)来说,除了 null 都可以显示正确的类型;对于对象来说,除了函数都会显示 object。
2、instanceof 是通过原型链来判断的。可以判断一个对象的正确类型,但是对于基本数据类型的无法判断。
3、instanceof能正确判断对象的原理:
通过判断对象的原型链中是不是能找到类型的原型 [].proto == Array.prototype
function myInstanceof(left, right) {
let prototype = right.prototype
left = left.__proto__
while (true) {
if (left === null || left === undefined)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
4、相关笔试题:
1)JavaScript中如何检测一个变量是一个String类型?请写出函数实现。
var str = 'ssssssss';
typeof str === 'string'
str.constructor === String
(2)闭包
1、闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数。
function a(){
var num = 100;
function b(){
console.log(num);
}
return b;
}
var c = a();
c(); //100
如上所示:函数b可以访问到函数a中的变量,函数b就是闭包。
2、闭包的用途:1)读取函数内部的变量;2)让这些变量始终保存在内存中
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议只在绝对必要时再考虑使用闭包。
3、相关面试题
1)循环中使用闭包解决‘var’定义函数的问题
for(var i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},2000)
}
这里因为setTimeout是异步执行,循环会先执行完毕,这时i等于6,然后就会输出5个6。解决方法如下所示:
第一种是使用let
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},2000)
}
第二种是使用闭包
for(var i = 1; i <= 5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},2000)
})(i)
}
这里首先使用了立即执行函数将i传入函数内部,这个时候值就被固定在了参数j上面不会改变,当下次执行timer这个闭包的时候,就可以使用外部函数的变量j。
(3)原型和原型链
1、我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调动构造函数而创建的那个对象实例的原型对象。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
示例:
function Person(){}
var p1 = new Person();
var p2 = new Person();
Person.prototype.name = 'CoCo';
console.log(p1.name); //CoCo
console.log(p2.name); //CoCo
Person构造函数下有一个prototype属性。Person.prototype就是原型对象,也就是实例p1、p2的原型。
2、在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。
function Person(){}
console.log(Person.prototype.constructor === Person); //true
3、Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__,这个属性对脚本是完全不可见的。__proto__用于将实例与构造函数的原型对象相连。
连接实例与构造函数的原型对象之间。
function Person(){}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); //true
//isPrototypeOf:检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中
console.log(Person.prototype.isPrototypeOf(p1)); //true
//getPrototypeOf:返回对象__proto__指向的原型prototype
console.log(Object.getPrototypeOf(p1) == Person.prototype); //true
(4)call、apply、bind
每个函数都包含两个非继承而来的方法: appy()和call()。这两个方法的用途都是在特定的作用域中调用函数,然后可以设置调用函数的this指向。
1、apply()第一个参数是this所要指向的那个对象,如果设为null或undefined或者this,则等同于指定全局对象。二是参数(可以是数组也可以是arguments对象)。
var a = 1;
function fn1(){
console.log(this.a);
}
var obj = {
a:2
};
fn1(); //1
fn1.apply(obj); //2
fn1.call(obj);//2
fn1.bind(obj)();//2
2、call()方法可以传递两个参数。第一个参数是指定函数内部中this的指向(也就是函数执行时所在的作用域),第二个参数是函数调用时需要传递的参数。第二个参数必须一个个添加。
3、bind()方法可以传递两个参数。第一个参数是指定函数内部中this的指向,第二个参数是函数调用时需要传递的参数。第二个参数必须一个个添加。
4、三者区别:
1)都可以在函数调用时传递参数,call、bind方法需要直接传入,而apply方法可以以数组或者arguments的形式传入。
2)call、apply方法是在调用之后立即执行函数,而bind方法没有立即执行,需要将函数再执行一遍。
5、手写三种函数
1)apply
Function.prototype.myApply = function(context){
if(typeof this !== 'function'){
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
let result;
if(arguments[1]){
result = context.fn(...arguments[1]);
}else{
result = context.fn();
}
delete context.fn;
return result;
};
2)call
Function.prototype.myCall = function(context){
if(typeof this !== 'function'){
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
const args = [...arguments].slice(1);
const result = context.fn(...args);
delete context.fn;
return result;
};
3)bind
Function.prototype.myBind = function(context){
if(typeof this !== 'function'){
throw new TypeError('Error');
}
const _this = this;
const args = [...arguments].slice(1);
return function F(){
if(this instanceof F){
return new _this(...args,...arguments);
}
return _this.apply(context,args.concat(...arguments));
}
};
bind返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new的方式:
直接调用,这里选择了apply的方式实现,因为因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)。
最后来说通过 new 的方式,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
(5)深拷贝浅拷贝
1、浅拷贝
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
基本数据类型是数据拷贝,会重新开辟一个空间存放拷贝的值。对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个情况。
1)概念:对于对象类型,浅拷贝就是对对象地址的拷贝,拷贝的结果是两个对象指向同一个地址,并没有开辟新的栈,修改其中一个对象的属性,另一个对象的属性也会改变。
2)实现:
a、通过Object.assign来实现。Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。用于对象的合并,将源对象的所有可枚举属性复制到目标对象。
Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
b、通过展开运算符 ... 来实现浅拷贝
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
c、循环实现
这里是浅拷贝,因为对象还是拷贝的地址。
var a = {
name:'coco',
age:33,
fn:function(){
console.log("111")
},
children:{
age:66
}
};
var b = {};
for(var i in a){
b[i] = a[i];
}
b.children.age = 100;
console.log(a);
console.log(b);
2、深拷贝
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。
1)概念:对于对象类型,深拷贝会开辟新的栈,两个对象对应两个不同的地址,修改其中一个对象的属性,另一个对象的属性不会改变。
2)原理:深复制--->实现原理,先新建一个空对象,内存中新开辟一块地址,把被复制对象的所有可枚举的(注意可枚举的对象)属性方法一一复制过来,注意要用递归来复制子对象里面的所有属性和方法,直到子子.....属性为基本数据类型。
3)实现:
a、通过 JSON.parse(JSON.stringify(object)) 来解决
缺点:可以满足基本的深拷贝,但是对于正则表达式、函数类型则无法进行拷贝。它还会抛弃对象的constructor。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
b、递归
var a = {
name:'kiki',
age:25,
children:{
name:'CoCo',
age:12
},
arr:['111','222'],
fn:function(){
console.log(1);
}
};
function copy(obj){
if(obj instanceof Array){
var newObj = [];
}else{
var newObj = {};
}
for(var i in obj){
if(typeof obj[i] == 'object'){
newObj[i] = copy(obj[i]);
}else{
newObj[i] = obj[i];
}
}
return newObj;
}
var b = copy(a);
console.log(a);
console.log(b);
c、jQuery.extend()
jQuery.extend([deep],target,object1,object2...),deep位Boolean类型,如果为true则进行深拷贝。