二、引用数据类型
引用类型有Object
,Function
,Array
,Date
,Math
等。
引用类型与基本数据类型的区别:
(1)引用数据类型的实例需要通过new
关键字创建。
(2)将引用数据类型赋值给变量,实际上赋值的是内存地址
(3)引用数据类型的比较是对内存地址的比较,而基本数据类型的比较是对值的比较。
1、Object类型
Object
类型是JavaScript
中使用最多的一个类型。
大部分的引用数据类型都是Object
类型。
由于引用数据类型的实例都是通过new
关键字来创建的,所以我们先来探讨有关new
操作相关的问题。
1.1 new 操作符的作用
new
操作符在执行过程中会改变this
的指向,所以下面我们先来看一下this
的用法。
<script>
function Person(userName, age) {
this.userName = userName;
this.age = age;
}
console.log(new Person("zhangsan", 20));
</script>
执行上面的代码,发现输出的是一个Person
对象,包含了userName
和age
的数据。
但是,问题是,在构造函数Person
中,我们没有添加return
,为什么会有返回值呢?
其实就是this
这个关键字起作用。
<script>
function Person(userName, age) {
console.log(this);//输出的是Person{ }对象
this.userName = userName;
this.age = age;
}
new Person("zhangsan", 20);
</script>
执行上面的代码,我们可以看到this
这里就是一个Person
的空对象,后面的两行代码就相当于给Person
对象添加了userName
和age
这两个属性。
下面我们把代码修改成如下的形式:
<script>
function Person(userName, age) {
var Person = {};
Person.userName = userName;
Person.age = age;
}
console.log(new Person("zhangsan", 20));
</script>
以上打印的结果中,输出的是Person{}
,并没有包含userName
和age
,原因是什么呢?
因为在 构造函数中如果没有添加return
,则默认返回的是return this
.
修改后的代码如下:
<script>
function Person(userName, age) {
var Person = {};
Person.userName = userName;
Person.age = age;
return Person;
}
console.log(new Person("zhangsan", 20));
</script>
对this
有了一个简单的了解以后,下面重点看如下代码
var person= new Person("zhangsan", 20)
从上面的代码中,this
关键字主要的作用就是创建一个Person
对象,然后赋值给了person
这个变量,该变量中包含了Person
对象中的属性和函数。
其实,在new
操作符做了如下3件事情。
var person={};
person.__proto__=Person.prototype;
Person.call(person)
1.2 原型对象理解
函数对象的 prototype 属性
我们创建的每一个函数都有一个 prototype
属性,这个属性是一个指针,指向一个对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,简单来说,该函数实例化的所有对象的__proto__
的属性指向这个对象,它是该函数所有实例化对象的原型。
function Person(){
}
// 为原型对象添加方法
Person.prototype.sayName = function(){
alert(this.name);
}
下面我们来看一下它们之间的关系。
简易图
constructor 属性
当函数创建,prototype
属性指向一个原型对象时,在默认情况下,这个原型对象将会获得一个 constructor 属性,这个属性是一个指针,指向 prototype
所在的函数对象。
拿前面的一个例子来说 Person.prototype.constructor
就指向 Person
函数对象。
console.log(Person.prototype.constructor == Person)
下面我们来更新一下它们之间的关系图。
简易图
对象的 __proto__
属性
当我们调用构造函数创建一个新实例后,在这个实例的内部将包含一个指针,指向构造函数的原型对象.
根据前面的 Person
构造函数我们新建一个实例
var student = new Person();
console.log(student.__proto__ === Person.prototype); // true
从上面我们可以看出,这个连接是存在与实例与构造函数的原型对象之间的,而不是存在于实例和构造函数之间的。
下面我们来看一下现在这几个对象之间的关系
isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上。
console.log(Person.prototype.isPrototypeOf(student)); // true
简易图
1.3 原型属性
属性访问
每当代码读取对象的某个属性时,首先会在对象本身搜索这个属性,如果找到该属性就返回该属性的值,如果没有找到,则继续搜索该对象对应的原型对象,以此类推下去。
因为这样的搜索过程,因此我们如果在实例中添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,因为在实例中搜索到该属性后就不会再向后搜索了。
属性判断
既然一个属性既可能是实例本身的,也有可能是其原型对象的,那么我们该如何来判断呢?
在属性确认存在的情况下,我们可以使用 hasOwnProperty()
方法来判断一个属性是存在与实例中,还是存在于原型中
function Person() {};
Person.prototype.name = "laker" ;
var student = new Person();
console.log(student.name); // laker
console.log(student.hasOwnProperty("name")); // false
student.name = "xiaoming";
console.log(student.name); //xiaoming 屏蔽了原型对象中的 name 属性
console.log(student.hasOwnProperty("name")); // true
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
所有属性获取
function Person() {
this.name = "KXY";
}
Person.prototype = {
job: "student",
};
var kxy = new Person();
Object.defineProperty(kxy, "sex", {
value: "female",
enumerable: false,
});
console.log(Object.keys(kxy)); //["name"] //无法获取不可枚举的属性与原型链上继承的属性
console.log(Object.getOwnPropertyNames(kxy));//["name", "sex"]
//for...in能获取原型链上继承的属性,无法获取不可枚举的属性
for (var pro in kxy) {
console.log("kxy." + pro + " = " + kxy[pro]);// kxy.name = KXY
//kxy.job = student
}
怎样判断属性是否为实例属性并且是否可枚举
如果想判断指定名称的属性是否为实例属性并且是否可枚举的,可以使用propertyIsEnumerable
function Student(userName) {
this.userName = userName;
}
Student.prototype.sayHello = function () {
console.log("hello" + this.userName);
};
var stu = new Student();
console.log(stu.propertyIsEnumerable("userName")); //true:userName为自身定义的实例属性
console.log(stu.propertyIsEnumerable("age")); // false:age属性不存在,返回false
console.log(stu.propertyIsEnumerable("sayHello")); // false :sayHello属于原型上的函数
//将userName属性设置为不可枚举
Object.defineProperty(stu, "userName", {
enumerable: false,
});
console.log(stu.propertyIsEnumerable("userName")); // false: userName设置了不可枚举
1.4 Object.create( )
方法
基本使用
该函数的主要作用是创建并返回一个指定原型和指定属性的新对象,语法格式如下:
Object.create(prototype,propertyDescriptor)
prototype
属性为对象的原型(必须),可以为null
,如果为null
,则对象的原型为undefined
.
propertyDescriptor
表示的是属性描述符(可选),具体的格式如下:
propertyName:{
value:'',
writable:true,
enumerable:true,
configurable:true
}
基本实现:
<script type="text/javascript">
const person = {
userName: "zhangsan",
sayHello: function () {
console.log("hello " + this.userName);
},
};
const stu = Object.create(person);
stu.userName = "lisi";
stu.sayHello(); //hello lisi 覆盖了person中的userName属性原有的值
</script>
通过以上的代码,可以看到stu
对象的原型是person
.也就是stu.__proto__===person
下面再来看一个案例:
var obj = Object.create(null, {
userName: {
value: "wangwu",
writable: true,
enumerable: true,
configurable: true,
},
age: {
value: 23,
},
});
console.log(obj.userName);
console.log(obj.age);
obj.age = 26;
console.log(obj.age);
for (var o in obj) {
console.log(o);
}
delete obj.userName;
console.log(obj.userName);
delete obj.age;
console.log(obj.age);
实现原理
通过如下的伪代码来查看对应的实现原理
Object.create=function(proto,propertiesObject){
//省略了其它判断操作
function F(){}
F.prototype=proto;
if(propertiesObject){ Object.defineProperties(F, propertiesObject)}
return new F()
}
通过以上的代码,我们可以得出如下的结论:
var f=new F()
f.__proto__===F.prototype
下面我们可以通过一个例子来验证一下:
var obj = { x: 12, y: 13 };
var test = Object.create(obj);
console.log(test);
console.log(test.x);
console.log(test.__proto__.x);
最后,这里演示一下Object.defineProperties
方法的基本使用
该方法的主要作用就是添加或修改对象的属性。
如下代码所示:
var person = {};
Object.defineProperties(person, {
userName: {
value: "张三",
enumerable: true,
},
age: {
value: 12,
enumerable: true,
},
});
for (var p in person) {
console.log(p);
}
person.age = 20;
console.log(person.age);
应用场景
对于Object.create
方法很重要的一个应用场景是用来实现继承
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.getInfo = function () {
console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};
var a = new Person("jojo", "femal");
var b = Object.create(Person.prototype);
console.log(a.name);
console.log(b.name);
console.log(b.getInfo);
下面看一下怎样实现完整的继承操作。
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.getInfo = function () {
console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};
function Student(name, sex, age) {
Person.call(this, name, sex);
this.age = age;
}
Student.prototype = Object.create(Person.prototype);
var s = new Student("coco", "femal", 25);
s.getInfo();
下面,我们简单的分析一下,上面的代码。
对象s
的__proto__
指向的是s
的构造函数Student
的prototype
s.__proto__===Student.prototype
那么Student.prototype
的__proto__
指向什么呢?
Student.prototype.__proto__===Person.prototype
s.__proto__.__proto__===Person.prototype
而我们知道对象s
是由Student
创建的,所以其构造函数为Student
,所以我们在修改了原型以后,这里应该重新修正构造函数。
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.getInfo = function () {
console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};
function Student(name, sex, age) {
Person.call(this, name, sex);
this.age = age;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
var s = new Student("coco", "femal", 25);
s.getInfo();
1.5 Object.create( )
与new Object()
的区别
1.6 模拟new
操作符的实现
在前面我们介绍了new
操作符所做的三件事情,下面我们来模拟实现一下。
function Person(name, age) {
this.name = name;
this.age = age;
}
function New() {
var obj = {};
var res = Person.apply(obj, arguments);
return typeof res === "object" ? res : obj;
}
console.log(New("zhangsan", 19));
1.7 原型链理解
下面我们通过一个案例来看一个简单的原型链过程。初步代码如下
var A=function(){ }
var a=new A( );
通过a
实例沿着原型链第一次的追溯,__proto__
属性指向A()
构造函数的原型对象。
a.__proto__===A.prototype
a
实例沿着原型链第二次的追溯,A
原型对象的__proto__
属性指向Object
类型的原型对象.
a.__proto__.__proto__===A.prototype.__proto__
A.prototype.__proto__===Object.prototype
a
实例沿着原型链第三次追溯,Object
类型的原型对象的__proto__
属性为null
a.__proto__.__proto__.__proto__===Object.prototype.__proto__
Object.prototype.__proto__===null
具体的图如下所示:
下面,我们再来看一个案例:
function Super(){
};
function Middle(){
};
function Sub(){
};
Middle.prototype = new Super();
Sub.prototype = new Middle();
var suber = new Sub();
对应的原型链如下图所示:
上面的图其实并不完整,因为漏掉了
Object
.所以完整的图如下
通过以上内容,我们对原型链有了更加深入的理解。
1.8 原型链特点
关于原型链的特点,主要有两个
第一个特点:由于原型链的存在,属性查找的过程不再只是查找自身的原型对象,而是会沿着整个原型链一直向上,直到追溯到Object.prototype
.也就是说,当js
引擎在查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,直到Object.prototype
.如果Object.prototype
上也找不到该属性,则返回undefined
,如果期间在对象本身找到了或者是某个原型对象上找到了该属性,则会返回对应的结果。
由于这个特点,我们在自定义的对象中,可以调用某些未在自定义构造函数中定义的函数,例如toString( )
函数。
function Person(){ }
var p = new Person();
p.toString(); // 实际上调用的是Object.prototype.toString( )
第二个特点:由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大。
1.9 属性的区分
对象属性的查找往往会涉及到整个原型链,那么应该怎样区分属性是实例自身的还是从原型链中继承的呢?
关于这个问题,前面我们是通过hasOwnProperty( )
函数来完成的,这里我们在简单的复习强调一下。
function Person(name, age) {
this.name = name;
}
//在对象的原型上添加age属性
Person.prototype.age = 21;
var p = new Person("zhangsan");
console.log(p.hasOwnProperty("name")); //true
console.log(p.hasOwnProperty("age")); //false
name
属性为实例属性,在调用hasOwnProperty
方法时,会返回true
。age
属性为原型对象上的属性,在调用hasOwnProperty
函数时,会返回false
.
在使用for...in
运算符,遍历对象的属性时,一般可以配合hasOwnProperty
方法一起使用,检测某个属性是否为对象自身的属性,如果是,可以做相应的处理。
for(var p in person){
if(person.hasOwnProperty(p)){
}
}
2、Array类型
Array
类型中提供了丰富的函数用于对数组进行处理,例如,过滤,去重,遍历等等操作。
2.1 怎样 判断一个变量是数组还是对象
这里,我们可能会想到使用typeof
运算符,因为typeof
运算符是专门用于检测数据类型的,但是typeof
运算符能够满足我们的需求吗?
var a = [1, 2, 3];
console.log(typeof a);
2.1.1 instanceof
运算符
instanceof
运算符用于通过查找原型链来检查某个变量是否为某个类型数据的实例,使用instanceof
运算符可以判断一个变量是数组还是对象。
var a = [1, 2, 3];
console.log(a instanceof Array); // true
console.log(a instanceof Object); // true
var userInfo = { userName: "zhangsan" };
console.log(userInfo instanceof Array); // false
console.log(userInfo instanceof Object); // true
这里我们可以封装一个函数,用于判断变量是数组类型还是对象类型。
var a = [1, 2, 3];
function getType(o) {
if (o instanceof Array) {
return "Array";
} else if (o instanceof Object) {
return "Object";
} else {
return "参数类型不是Array也不是Object";
}
}
console.log(getType(a));
2.1.2 通过构造函数来判断
判断一个变量是否是数组还是对象,其实就是判断变量的构造函数是Array
类型还是Object
类型。
因为一个对象的实例都是通过构造函数创建的。
var a = [1, 2, 3];
console.log(a.__proto__.constructor === Array);
console.log(a.__proto__.constructor === Object); // false
同样这里,这里我们也可以封装一个函数,来判断变量是数组类型还是对象类型。
function getType(o) {
//获取构造函数
var constructor = o.__proto__.constructor;
if (constructor === Array) {
return "Array";
} else if (constructor === Object) {
return "Object";
} else {
return "参数类型不是Array也不是Object";
}
}
var a = [1, 2, 3];
console.log(getType(a));
2.1.3 通过toString( )
函数来判断
我们知道,每种引用类型都会直接或间接继承Object
类型,因此它们都包含toString( )
函数。
不同数据类型的toString( )
函数返回值也不一样,所以通过toString( )
函数就可以判断一个变量是数组还是对象,当然,这里我们需要用到call
方法来调用Object
原型上的toString( )
函数来完成类型的判断。
如下所示:
var arr = [1, 2, 3];
var obj = { userName: "zhangsan" };
console.log(Object.prototype.toString.call(arr)); //[object Array]
console.log(Object.prototype.toString.call(obj)); // [object Object]
console.log(arr.toString()); // 1,2,3
2.1.4 通过Array.isArray( )
函数来判断
Array.isArray
方法用来判断变量是否为数组。
var arr = [1, 2, 3];
var obj = { name: "zhangsan" };
console.log(Array.isArray(1)); //false
console.log(Array.isArray(arr)); //true
console.log(Array.isArray(obj)); //false
2.2 怎样过滤数组中满足条件的数据
对数组中的数据进行过滤,我们使用比较多的是filter
方法。
<script>
var fn = function (x) {
return x % 2 !== 0;
};
var arr = [1, 2, 5, 6, 78, 9, 10];
var result = arr.filter(fn);
console.log(result);
</script>
下面,我们再来看一下针对复杂类型数组的过滤。
下面案例是查找出年龄大于16的男生的信息。
var arr = [
{ gender: "男", age: 15 },
{ gender: "男", age: 17 },
{ gender: "女", age: 15 },
];
var fn = function (obj) {
return obj.gender === "男" && obj.age > 16;
};
const result = arr.filter(fn);
console.log(result);
2.3 怎样对数组元素做累加处理
对数组中的元素做累加的处理,可以通过reduce
函数来完成。
reduce
函数最主要的作用就是做累加的操作,该函数接收一个函数作为累加器,将数组中的每个元素从左到右依次执行累加器,返回最终的处理结果。
reduce
函数的语法如下:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
求出数组中所有元素累加的和
var arr = [1, 2, 3, 4, 5, 6];
var sum = arr.reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
console.log(sum);
2.4 怎样求数组中的最大值与最小值
关于查询出数组中的最大值与最小值的实现方式有很多种,下面我们来看一下具体的实现。
第一:通过prototype
属性扩展min
函数和max
函数来实现求最小值与最大值
//最小值
Array.prototype.min = function () {
var min = this[0];
var len = this.length;
for (var i = 1; i < len; i++) {
if (this[i] < min) {
min = this[i];
}
}
return min;
};
//最大值
Array.prototype.max = function () {
var max = this[0];
var len = this.length;
for (var i = 1; i < len; i++) {
if (this[i] > max) {
max = this[i];
}
}
return max;
};
var arr = [1, 3, 6, 90, 23];
console.log(arr.min()); // 1
console.log(arr.max()); // 90
第二:通过数组的reduce
函数来完成。
Array.prototype.max = function () {
return this.reduce(function (preValue, currentValue) {
return preValue > currentValue ? preValue : currentValue; //返回最大的值
});
};
Array.prototype.min = function () {
return this.reduce(function (preValue, currentValue) {
return preValue < currentValue ? preValue : currentValue; // 返回最小的值
});
};
var arr = [1, 3, 6, 90, 23];
console.log(arr.min()); //
console.log(arr.max()); //
第三:通过ES6
中的扩展运算符来实现
这里我们可以通过ES6
中的扩展运算符(...)来实现。
var arr = [1, 3, 6, 90, 23];
console.log(Math.min(...arr)); //
console.log(Math.max(...arr));
2.5 数组遍历的方式有哪些
数组遍历是我们针对数组最频繁的操作。下面我们看一下常见的数组的遍历方式。
通过for循环
这时最基本的实现方式
var arr=[1,2,3]
for(var i=0;i<arr.length;i++){
console.log(arr[i])
}
使用forEach( )
函数
forEach
函数也是我们遍历数组用的比较多的方法,forEach( )
函数接收一个回调函数,参数分别表示当前执行的元素的值,当前值的索引和数组本身。
var arr = [1, 3, 6, 90, 23];
arr.forEach(function (element, index, array) {
console.log(index + ":" + element);
});
使用map( )
函数
`m
var arr = [1, 3, 6, 90, 23];
var result = arr.map(function (element, index, array) {
console.log(index);
return element * element;
});
console.log("result: ===", result);
在使用map
函数的时候一定要注意:在map( )
函数的回调函数中需要通过return
将处理后的值进行返回,否则会返回undefined
.
如下所示:
var arr = [1, 3, 6, 90, 23];
var result = arr.map(function (element, index, array) {
// console.log(index);
element * element;
});
console.log("result: ===", result);
在上面的计算中,将return
关键字省略了,最终返回的结果是:
[undefined, undefined, undefined, undefined, undefined]
使用some( )
函数与every( )
函数
some( )
函数与every( )
函数的相似之处都是在对数组进行遍历的过程中,判断数组中是否有满足条件的元素,如果有满足条件的就返回true
,否则返回false
.
some()
与every()
的区别在于:some( )
函数只要数组中某个元素满足条件就返回true
,不会在对后面的元素进行判断。而every( )
函数是数组中每个元素都要满足条件时才会返回true
.
例如:要判断数组中是否有大于6的元素的时候,可以通过some( )
函数来处理。
而要判断数组中是否所有的元素都大于6,则需要通过every( )
函数来处理。
function fn(element, index, array) {
return element > 6;
}
var result = [1, 2, 3, 4, 5].some(fn); //false
console.log(result);
var result = [1, 2, 3, 4, 5, 7].some(fn);
console.log(result);
下面测试一下every( )
函数
function fn(element, index, array) {
return element > 6;
}
var result = [1, 2, 3, 4, 5, 7].every(fn); //false
console.log(result);
下面修改一下数组中的元素。
function fn(element, index, array) {
return element > 6;
}
var result = [7, 8].every(fn); //true
console.log(result);
现在数组中的元素的值都是大于6,所以返回的结果为true
.
使用find( )
函数
find( )
函数用于数组的遍历,当找到第一个满足条件的元素值时,则直接返回该元素值,如果都找不到满足条件的,则返回undefined
.
find( )
方法的参数与forEach
是一样的。
var arr = [1, 3, 6, 90, 23];
const result = arr.find(function (element, index, array) {
return element > 6;
});
console.log(result); // 90
var arr = [1, 3, 6, 90, 23];
const result = arr.find(function (element, index, array) {
return element > 100; //undefined
});
console.log(result);
以上就是我们比较常用的数组遍历的方式。当然还有我们前面说过的filter
,reduce
函数。
2.6 手动实现find
方法
<script>
Array.prototype.findTest = function (fn) {
for (var i = 0; i < this.length; i++) {
var f = fn(this[i]);//把数组元素传递到函数中
if (f) { //如果函数的返回值为true
return this[i]; //则返回对应的数组元素
}
}
};
var arr = [1, 3, 6, 90, 23];
var result = arr.findTest(function (item) {
return item > 6;
});
console.log(result);
</script>
2.7 手动实现filter方法
filter
函数内部需要一个回调函数,数组中的每个元素都会执行该回调函数,在执行回调函数时会将数组中的每个元素传递给回调函数的参数,在回调函数的函数体内进行判断,如果返回的是true
,那么将该元素放到新数组arr
中,如果判断的结果为false
,则数据不会放到新数组arr
中。
//模拟实现filter函数
Array.prototype.filterOne = function (fn) {
var newArray = [];
for (var i = 0; i < this.length; i++) {
var f = fn(this[i]);
if (f) {
newArray.push(this[i]);
}
}
return newArray;
};
var array = [65, 56, 89, 53];
var arr = array.filterOne(function (item) {
return item >= 60;
});
console.log("arr=", arr);
2.8 手动实现some函数
some()
方法让数组中的每一个元素执行一次回调函数,在该回调函数中执行一些操作,只要有一个操作结果为真,就会返回true。不会在对后面的元素进行判断,否则返回false。
//手动模式some方法
Array.prototype.someTest = function (fn) {
for (let i = 0; i < this.length; i++) {
let f = fn(this[i]);
if (f) {
return f;
}
}
return false;
};
let array = [1, 3, 5, 7, 90];
let result = array.someTest(function (item) {
return item > 10;
});
console.log("result=", result);
2.9 手动实现every函数
该方法与some()
方法不同,some()
方法只要有一个符合条件就返回true,而 every()
方法是数组中所有元素都要符合指定的条件,才会返回true.
//手动模拟实现`every`方法
Array.prototype.everyTest = function (fn) {
let f = true;
for (let i = 0; i < this.length; i++) {
let f = fn(this[i]);
if (!f) {
//只要有一个不符合,就立即返回false.
return false;
}
}
return f;
};
let array = [11, 31, 5, 71, 90];
let result = array.everyTest(function (item) {
return item > 10;
});
console.log("result=", result); //false
2.10 手动实现map方法
map( )
函数在对数据进行遍历的时候,会将数组中的每个元素做相应的处理,然后得到新的元素,并返回一个新的数组。
//手动实现map方法
Array.prototype.mapTest = function (fn) {
let newArray = [];
for (let i = 0; i < this.length; i++) {
let f = fn(this[i], i, this);
newArray.push(f);
}
return newArray;
};
var arr = [1, 3, 6, 90, 23];
var result = arr.mapTest(function (element, index, array) {
console.log(index);
return element * element;
});
console.log("result: ===", result);
2.11 手动实现reduce方法
Array.prototype.reduceTest = function (fn, initialValue) {
//如果没有传递initialValue,我们将使用数组的第一项作为initialValue的值
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : this[0];
//如果没有传递initialValue,则索引从1开始,否则从0开始
for (let i = hasInitialValue ? 0 : 1, len = this.length; i < len; i++) {
value = fn(value, this[i], i, this);
}
return value;
};
var arr = [1, 2, 3, 4, 5, 6];
var sum = arr.reduceTest(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
console.log(sum);
2.12 怎样实现数组的去重
数组去重是指当数组中出现重复的元素的时候,通过一定的方式,将重复的元素去掉。
利用数组遍历去重
// 数组去重
function fn(array) {
var newArray = [];
for (var i = 0; i < array.length; i++) {
if (newArray.indexOf(array[i]) === -1) {
newArray.push(array[i]);
}
}
return newArray;
}
var arr = [1, 2, 3, 4, 5, 5, 6];
console.log(fn(arr));
利用键值对去重
function fn(array) {
var obj = {},
result = [],
val;
for (var i = 0; i < array.length; i++) {
val = array[i];
if (!obj[val]) {//根据key获取obj对象中的值
obj[val] = "ok"; //表示该元素已经出现了
result.push(val);
}
}
return result;
}
var arr = [1, 2, 3, 4, 5, 5, 6];
console.log(fn(arr));
function fn(array) {
var obj = {},
result = [],
val,
type;
for (var i = 0; i < array.length; i++) {
val = array[i];
type = typeof val;
if (!obj[val]) {
obj[val] = [type];
result.push(val);
} else if (obj[val].indexOf(type) < 0) {
obj[val].push(type);
result.push(val);
}
}
return result;
}
var arr = [1, 2, 3, 4, 5, 5, 6, "6"];
console.log(fn(arr));
使用Set
数据结构去重
具体的代码如下所示:
function fn(arr) {
return Array.from(new Set(arr));
}
console.log(fn([1, 2, 3, 4, 5, 5, 6, "6"]));
2.13 怎样获取数组中最多的元素
利用键值对实现
<script>
function fn(arr) {
//如果数组中没有值,直接返回
if (!arr.length) return;
//如果只有一个值,返回1,表示出现了1次
if (arr.length === 1) return 1;
var result = {};
//对数组进行遍历
for (var i = 0; i < arr.length; i++) {
if (!result[arr[i]]) {
result[arr[i]] = 1;
} else {
result[arr[i]]++;
}
}
//遍历result对象
var keys = Object.keys(result);
var maxNum = 0,
maxElement;
for (var i = 0; i < keys.length; i++) {
if (result[keys[i]] > maxNum) {
maxNum = result[keys[i]];
maxElement = keys[i];
}
}
return (
"在数组中出现最多的元素是" + maxElement + ",共出现了" + maxNum + "次"
);
}
var array = [1, 2, 3, 3, 3, 6, 6, 6, 6, 6, 7, 8, 9];
console.log(fn(array));
</script>
算法优化
function fn(array) {
var result = {};
var maxNum = 0;
var maxElement = null;
for (var i = 0; i < array.length; i++) {
var val = array[i];
result[val] === undefined ? (result[val] = 1) : result[val]++;
if (result[val] > maxNum) {
maxNum = result[val];
maxElement = val;
}
}
return (
"在数组中出现最多的元素是" + maxElement + ",共出现了" + maxNum + "次"
);
}
var array = [1, 2, 3, 3, 3, 6, 6, 6, 6, 6, 7, 8, 9];
console.log(fn(array));
三、函数
1、函数定义有哪几种实现方式
在使用函数前,先需要对函数进行定义。关于函数的定义总体上可以分为三类。
第一类是函数声明。
第二类是函数表达式
第三类是通过Function
构造函数来完成函数的定义。
首先来看一下函数的声明。
函数声明是直接通过function
关键字接一个函数名,同时可以接收参数。
function sum(num1, num2){
return num1 + num2
}
函数表达式
函数表达式的形式类似于普通变量的初始化,只不过这个变量初始化的值是一个函数。如下代码所示:
var sum = function (num1,num2){
return num1 + num2
}
这个函数表达式没有名称,属于匿名函数表达式。
Function( )
构造函数
使用new
操作符,调用Function( )
构造函数,传入参数,也可以定义一个函数。
var sum = new Function('num1','num2', 'return a+b ')
其中的参数,除了最后一个参数是要执行的函数体,其它的参数都是函数的形参。
2、Function( )构造函数定义函数的问题
但是,我们在实际的应用中很少使用Function( )
构造函数来实现对函数的定义。
原因是:
第一:Function( )
构造函数每次执行时,都会解析函数体,并创建一个新的函数对象,所以当在一个循环或者是一个频繁执行的函数中去使用Function( )
构造函数的时候,相对来说性能是比较低的。
第二:通过Function( )
构造函数创建的函数,并不遵循典型的作用域。
如下代码所示:
var a = "12";
function fun() {
var a = "11";
return new Function("return a");
}
console.log(fun()());
3、函数表达式的应用场景
关于函数表达式非常典型的应用就是实现了块级作用域
var person = (function () {
var _name = "";
return {
getName: function () {
return _name;
},
setName: function (userName) {
_name = userName;
},
};
})();
person.setName("zhangsan");
console.log(person.getName());
4、函数声明与函数表达式有什么区别
函数声明与函数表达式虽然是两种定义函数的方式,但是两者之间还是有区别的。
第一点就是:函数名称
// 函数声明,函数名称sum是必须的
function sum (num1,num2){
return num1 + num2
}
// 没有函数名称的匿名函数表达式
var sum = function (num1,num2){
return num1 + num2
}
第二点就是关于:函数提升
console.log(add(1, 2)); // 3
console.log(sum(3, 6)); // Uncaught TypeError: sum is not a function
// 函数声明
function add(num1, num2) {
return num1 + num2;
}
// 函数表达式
var sum = function (num1, num2) {
return num1 + num2;
};
5、函数常见的调用模式有哪些
函数调用模式
function add(num1, num2) {
return num1 + num2;
}
// 函数表达式
var sum = function (num1, num2) {
return num1 + num2;
};
console.log(add(1, 2));
console.log(sum(3, 6));
方法调用模式
var obj = {
userName: "zhangsan",
getUserName: function () {
return this.userName;
},
};
console.log(obj.getUserName());
var obj = {
userName: "zhangsan",
getUserName: function () {
return this.userName;
},
};
// console.log(obj.getUserName());
console.log(obj["getUserName"]());
var obj = {
userName: "zhangsan",
getUserName: function () {
return this.userName;
},
setUserName: function (name) {
this.userName = name;
return this;
},
};
console.log(obj.setUserName("lisi").getUserName());// lisi
构造器(构造函数)调用模式
//定义构造函数
function Person(name) {
this.userName = name; //定义属性
}
// 在原型上定义函数
Person.prototype.getUserName = function () {
return this.userName;
};
// 通过new来创建实例
var p = new Person("zhangsan");
// 调用原型上的方法
console.log(p.getUserName());
function sum(num1, num2) {
return num1 + num2;
}
//定义一个对象
var obj = {};
//通过call()和apply( )函数调用sum( )函数
console.log(sum.call(obj, 2, 6));
console.log(sum.apply(obj, [3, 6]));
匿名函数调用模式
所谓的匿名函数,就是没有函数名称的函数。匿名函数的调用有两种方式,一种是通过函数表达式定义函数,并赋值给变量,通过变量进行调用。如下所示:
//通过函数表达式定义匿名函数,并赋值给变量sum
var sum =funciton (num1,num2){
return num1 + num2
}
// 通过sum来进行调用
sum(2,6)
另外一种是使用小括号()
将匿名函数括起来,然后在后面使用小括号( )
,传递对应的参数从而完成对应的调用。
(function (num1, num2) {
console.log(num1 + num2);
})(2, 6);
6、实参与形参有哪些区别
第一:在函数的调用过程中,数据传递是单向的,也就是只能把实参的值传递给形参,而不能把形参的值反向传递给实参
第二:当实参是基本数据类型的值的时候,在向形参传递的时候,实际上是将实参的值复制一份传递给形参,在函数运行结束以后
形参释放,而实参中的值不会发生变化。当实参是引用类型的值的时候,实际是将实参的内存地址传递给形参,即实参与形参都指向了
相同的内存地址,此时形参可以修改实参的值。
var person = { age: 21 };
function fn(obj) {
obj.age = 22;
}
fn(person);
console.log(person.age);
第三:函数可以不用定义形参,在函数体中可以通过arguments
对象获取传递过来的实参的值,并进行处理。
第四:在函数定义形参时,形参的个数并一定要和实参的个数相同,实参与形参会按照从前向后的顺序进行匹配,没有匹配到的形参被当作undefined
来处理。
第五:实参并不需要与形参的数据类型一致,因为形参的数据类型只能在执行的时候才能够被确定,因为会通过隐式数据类型的转换。
7、介绍一下arguments对象
arguments
对象是所有函数都具有的一个内置的局部变量,表示的是函数实际接收到的参数,是一个类似数组的结构。
下面我们说一下arguments
对象都具有哪些性质。
第一:arguments
对象只能在函数内部使用,无法在函数的外部访问到arguments
对象。同时arguments
对象存在于函数级的作用域中。
console.log(arguments); //Uncaught ReferenceError: arguments is not defined
function fn() {
console.log(arguments.length);
}
fn(1, 2, 3);
第二:可以通过索引来访问arguments
对象中的内容,因为arguments
对象类似数组结构。
function fn() {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // undefined
}
fn(1, 2);
第三:arguments
对象的值由实参决定,不是由形参决定。
function fn(num1, num2, num3) {
console.log(arguments.length); // 2
}
fn(1, 2);
因为arguments
对象的length
属性是由实际传递的实参的个数决定的,所以这里输出的是2.
function fn(num1, num2, num3) {
arguments[0] = 23;
console.log("num1=", num1); //23
num2 = 33;
console.log(arguments[1]); // 33
}
fn(1, 2);
function fn(num1, num2, num3) {
// arguments[0] = 23;
// console.log("num1=", num1); //23
// num2 = 33;
// console.log(arguments[1]); // 33
arguments[2] = 19;
console.log(num3); //undefined
num3 = 10;
console.log(arguments[2]); // 19
}
fn(1, 2);
function fn(num1, num2, num3) {
// arguments[0] = 23;
// console.log("num1=", num1); //23
// num2 = 33;
// console.log(arguments[1]); // 33
arguments[2] = 19;
console.log(num3); //undefined
num3 = 10;
console.log(arguments[2]); // 19
console.log(arguments.length); // 2 长度还是2
}
fn(1, 2);
8、arguments对象有哪些应用场景
第一:进行参数个数的判断。
function fn(num1, num2, num3) {
// 判断传递的参数个数是否正确
if (arguments.length !== 3) {
throw new Error(
"希望传递3个参数,实际传递的参数个数为:" + arguments.length
);
}
}
fn(1, 3);
第二:对任意个数参数的处理,也就是说只会对函数中前几个参数做特定处理,后面的参数不论传递多少个都会统一进行处理,这种情况我们可以使用arguments
对象来完成。
function fn(sep) {
var arr = Array.prototype.slice.call(arguments, 1);
// console.log(arr); // ["a", "b", "c"]
return arr.join(sep);
}
console.log(fn("-", "a", "b", "c"));
第三:模拟函数的重载
什么是函数的重载呢?
函数的重载指的是在函数名称相同的情况下,函数的形参的类型不同或者是个数不同。
但是在JavaScript
中没有函数的重载。
function fn(num1, num2) {
return num1 + num2;
}
function fn(num1, num2, num3) {
return num1 + num2 + num3;
}
console.log(fn(1, 2)); // NaN
console.log(fn(1, 2, 3)); // 6
function fn() {
//将arguments对象转换成数组
var arr = Array.prototype.slice.call(arguments);
// console.log(arr); // [1,2]
//调用数组中的reduce方法完成数据的计算
return arr.reduce(function (pre, currentValue) {
return pre + currentValue;
});
}
console.log(fn(1, 2));
console.log(fn(1, 2, 3));
console.log(fn(1, 2, 3, 4, 5));
9、说一下普通函数与构造函数的区别
在JavaScript
的函数中,有一类比较特殊的函数:'构造函数'。当我们创建对象的时候,经常会使用构造函数。
构造函数与普通函数的区别:
第一:构造函数的函数名的第一字母通常会大写。
第二:在构造函数的函数体内可以使用this
关键字,表示创生成的对象实例。
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
console.log(person);
第三:在使用构造函数的时候,必须与new
操作符配合使用。
第四:构造函数的执行过程与普通函数也是不一样的。
代码如下:
function Person(userName) {
this.userName = userName;
this.sayHi = function () {
console.log(this.username);
};
}
var p1 = new Person("zhangsan");
var p2 = new Person("lisi");
console.log(p1.sayHi === p2.sayHi); // false
function Person(userName) {
this.userName = userName;
// this.sayHi = function () {
// console.log(this.username);
// };
}
Person.prototype.sayHi = function () {
console.log(this.username);
};
var p1 = new Person("zhangsan");
var p2 = new Person("lisi");
console.log(p1.sayHi === p2.sayHi); // true
10、什么是变量提升,什么是函数提升
在javascript
中存在一些比较奇怪的现象。在一个函数体内,变量在定义之前就可以被访问到,而不会抛出异常。
如下所示:
function fn() {
console.log(num); // undefined
var num = 2;
}
fn();
同样函数在定义之前也可以被调用,而不会抛出异常。
如下代码所示:
fn();
function fn() {
console.log("hello");
}
导致出现以上情况的原因是,在javascript
中存在变量提升与函数提升的机制。
在了解变量提升之前,先来说说作用域的问题。
作用域
在JavaScript
中,一个变量的定义与调用都是在一个固定的范围内的,这个范围我们称之为作用域。
作用域可以分为全局的作用域,局部作用域(函数作用域和块级作用域)。
如下程序:
function fn() {
var userName = "zhangsan";
console.log(userName);
}
fn(); //zhangsan
下面,再看如下代码:
var userName = "zhangsan";
function fn() {
console.log(userName);
}
fn(); //zhangsan
综上两个案例,我们可以总结出,作用域本质就是一套规则,用于确定在何处以及如何查找变量的规则。
下面,我们再来看一个比较复杂的结构图,来体验一下作用域
- 作用域链
下面,我们再来看一下前面的代码:
var userName = "zhangsan";
function fn() {
console.log(userName);
}
fn(); //zhangsan
我们在查找userName
这个变量的时候,现在函数的作用域中进行查找,没有找到,再去全局作用域中查找。你会注意到,这是一个往外层查找的过程,即顺着一条链条从下往上查找变量。这个链条,我们就称之为作用域链。
如下图所示:
对应的代码如下:
面试中关于作用域与作用域链的问题
第一题:以下代码的执行结果是:
var a = 1;
function fn1() {
function fn2() {
console.log(a);
}
function fn3() {
var a = 4;
fn2();
}
var a = 2;
return fn3;
}
var fn = fn1();
fn(); // 2
第二题:以下代码的执行结果是:
var a = 1;
function fn1() {
function fn3() {
var a = 4;
fn2();
}
var a = 2;
return fn3;
}
function fn2() {
console.log(a);
}
var fn = fn1();
fn(); // 1
第三题:以下代码的输出结果为
var a = 1;
function fn1() {
function fn3() {
function fn2() {
console.log(a);
}
var a;
fn2();
a = 4;
}
var a = 2;
return fn3;
}
var fn = fn1();
fn(); //undefined
第四题:以下代码的输出结果为:
var x = 10;
bar(); //10
function foo() {
console.log(x);
}
function bar() {
var x = 30;
foo();
}
第五题: 以下代码的输出结果为:
var x = 10;
bar(); //30
function bar() {
var x = 30;
function foo() {
console.log(x);
}
foo();
}
第六题:以下代码的输出结果为:
var x = 10;
bar(); //30
function bar() {
var x = 30;
(function () {
console.log(x);
})();
}
变量提升
所谓变量提升,是将变量的声明提升到函数顶部的位置,也就是将变量声明提升到变量所在的作用域的顶端,而变量的赋值并不会被提升。
var str = "hello world";
(function () {
console.log(str);
var str = "hello vue";
})(); // undefined
var str = "hello world";
(function () {
var str; //变量的声明得到提升
console.log(str);
str = "hello vue"; // 变量的赋值没有得到提升
})();
如下代码所示:
(function () {
console.log(str);
str = "hello vue";
})(); // str is not defined
以下代码的执行结果是:
function foo() {
var a = 1;
console.log(a); //1
console.log(b); //undefined
var b = 2;
}
foo();
上面的代码等价于
function foo() {
var a;
var b;
a = 1;
console.log(a); // 1
console.log(b); // undefined
b = 2;
}
foo();
函数提升
不仅通过var
定义的变量会出现提升的情况,使用函数声明方式定义的函数也会出现提升。
如下代码:
foo(); // 函数提升
function foo() {
console.log("hello");
}
function foo(){
console.log("hello");
}
foo() //'hello'
foo(); // foo is not a function
var foo = function () {
console.log("hello");
};
看一下如下程序的执行结果:
function foo() {
function bar() {
return 3;
}
return bar();
function bar() {
return 9;
}
}
console.log(foo()); // 9
如下程序的执行结果:
var a = true;
foo();
function foo() {
if (a) {
var a = 20;
}
console.log(a); // undefined
}
以上的代码的执行过程如下:
var a;
a = true;
function foo(){
var a;
if(a){
a=20
}
console.log(a)
}
foo()
如下程序的执行结果:
function v() {
var a = 1;
function a() {}
console.log(a);
}
v(); // 1
下面我们再来看一段代码:
function fn() {
console.log(typeof foo); // function
var foo = "hello";
function foo() {
return "abc";
}
console.log(typeof foo); // string
}
fn();
执行上面的代码,首先打印的是function
,然后是string
.
上面的代码实际上可以修改成如下的代码段。
function fn1() {
// 变量提升到函数的顶部
var foo;
// 函数提升,但是优先级低,所以出现在变量声明的后面。
function foo() {
return "abc";
}
console.log(typeof foo); //function
foo = "hello";
console.log(typeof foo); //string
}
下面,我们再来看一段代码,看一下对应的输出结果是:
function foo() {
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a);
}
foo(); //1
上面的代码可以修改成如下的代码。
function foo() {
//变量a提升
var a;
//函数声明b的提升
function b() {
//内部的函数声明a的提升
function a() {}
//全局变量
a = 10;
return;
}
a = 1;
b();
console.log(a);//在当前的作用域中,可以找到变量a,不需要获取全局变量a,所以其值为1,所以打印结果为1,
}
foo();
11、闭包
在正常的情况下,如果定义了一个函数,就会产生一个函数作用域,在函数体中的局部变量会在这个函数的作用域中使用。
一旦函数执行完毕后,函数所占用的空间就会被回收,存在于函数体中的局部变量同样也会被回收,回收后将不能被访问。
如果我们期望在函数执行完毕以后,函数中的局部变量仍然可以被访问到,应该怎样实现呢?
这里我们可以通过闭包来实现。
在了解闭包的问题之前,我们先说一个概念,执行上下文环境。
执行上下文环境
JavaScript
的每段代码的执行都会存在于一个执行上下文环境中。
执行上下文有且只有三类,全局执行上下文,函数上下文,与eval
上下文;由于eval
一般不会使用,这里不做讨论
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1();//3 2 1
为了方便理解,我们假设执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,因此过程大致如下:
//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
什么是闭包
关于闭包的官方概念:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。
简单的理解就是:闭包就是能够读取其它函数内部变量的函数。由于在JavaScript
语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
function outer () {
...
function inner () {
...
}
}
所以,本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包有两个比较显著的特点:
第一:函数拥有对外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
第二:闭包作为一个函数返回时,其执行上下文环境不会销毁,仍然处于执行上下文环境中。
在JavaScript
中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
下面,我们再来看另外一段代码:
function fn() {
var max = 10;
return function bar(x) {
if (x > max) {
console.log(x);
}
};
}
var f1 = fn();
f1(11); // 11
闭包的应用场景
应用缓存
var cacheApp = (function () {
var cache = {};
return {
getResult: function (id) {
// 如果在内存中,则直接返回
if (id in cache) {
return "缓存得到的结果为:" + cache[id];
}
//经过耗时函数的处理
var result = timeFn(id);
//更新缓存
cache[id] = result;
//返回计算的结果
return "得到的结果为:" + result;
},
};
})();
//耗时函数
function timeFn(id) {
console.log("这是一个非常耗时的任务");
return id;
}
console.log(cacheApp.getResult(23));
console.log(cacheApp.getResult(23));
代码封装
在编程的时候,我们提倡将一定特征的代码封装到一起,只需要对外暴露对应的方法就可以,从而不用关心内部逻辑的实现。
<script>
var stack = (function () {
//使用数组模拟栈
var arr = [];
return {
push: function (value) {
arr.push(value);
},
pop: function () {
return arr.pop();
},
size: function () {
return arr.length;
},
};
})();
stack.push("abc");
stack.push("def");
console.log(stack.size()); // 2
console.log(stack.pop()); // def
console.log(stack.size()); // 1
</script>
闭包常见面试题
第一:如下程序执行的结果为:
获取所单击的li
元素的索引值
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
<li>e</li>
</ul>
对应的js
代码如下:
// 获取所单击的`li`元素的索引值
var list = document.getElementsByTagName("ul")[0].children;
for (var i = 0; i < list.length; i++) {
list[i].onclick = function () {
console.log(i);
};
}
可以采用闭包解决这个问题:
var list = document.getElementsByTagName("ul")[0].children;
for (var i = 0; i < list.length; i++) {
(function (index) {
list[index].onclick = function () {
console.log(index);
};
})(i);
}
第二:如下程序输出结果是:
var arr = ["a", "b", "c"];
for (var i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(arr[i]);
}, 1000);
}
代码修改后的内容为:
var arr = ["a", "b", "c"];
for (var i = 0; i < arr.length; i++) {
(function (index) {
setTimeout(function () {
console.log(arr[index]);
}, 1000);
})(i);
}
第三:以下程序打印结果是:
var userName = "zhangsan";
var person = {
userName: "lisi",
method: function () {
return function () {
return this.userName;
};
},
};
console.log(person.method()()); //zhangsan
var userName = "zhangsan";
var person = {
userName: "lisi",
method: function () {
var that = this; //用that保存person的this
return function () {
return that.userName;
};
},
};
console.log(person.method()());
第四:以下程序的输出结果
function create() {
var a = 100;
return function () {
console.log(a);
};
}
var fn = create();
var a = 200;
fn(); // 100
第五:以下程序的输出结果:
function print(fn) {
var a = 200;
fn();
}
var a = 100;
function fn() {
console.log(a); // 100
}
print(fn);
闭包优缺点
闭包的优点:
第一:保护函数内变量的安全,实现封装,防止变量流入其它环境发生命名冲突,造成环境污染。
第二:在适当的时候,可以在内存中维护变量并缓存,提高执行效率
闭包的缺点:
消耗内存:通常来说,函数的活动对象会随着执行上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,所以说,闭包比一般的函数需要消耗更多的内存。
12、this指向
常见面试题
我们知道,当我们创建一个构造函数的实例的时候,需要通过new
操作符来完成创建,当创建完成后,函数体中的this
指向了这个实例。
如下代码所示:
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
console.log(person.userName);
如果,我们将上面的Person
函数当作一个普通函数来调用执行,那么对应的this
会指向谁呢?
function Person(userName) {
this.userName = userName;
}
Person("lisi");
console.log(window.userName);
通过上面的程序,我们可以总结出,this
指向的永远是函数的调用者。
第一:如下程序的输出结果:
var a = 10;
var obj = {
a: 120,
method: function () {
var bar = function () {
console.log(this.a); // 10
};
bar();//这里是通过window对象完成bar方法的调用
return this.a;
},
};
console.log(obj.method()); // 120
第二:如下程序的输出结果是:
var num = 10;
function Person() {
//给全局变量重新赋值
num = 20;
// 实例变量
this.num = 30;
}
Person.prototype.getNum = function () {
return this.num;
};
var person = new Person();
console.log(person.getNum()); // 30
第三:如下程序的输出结果是:
function fn() {
console.log(this);
}
let obj = {
fn: fn,
};
fn(); //window
obj.fn(); //obj
第四:如下程序的输出结果是:
var fullName = "language";
var obj = {
fullName: "javascript",
prop: {
getFullName: function () {
return this.fullName;
},
},
};
console.log(obj.prop.getFullName()); // undefined
var test = obj.prop.getFullName; // language
console.log(test());
第五:如下程序的输出结果是:
var val = 1;
var json = {
val: 10,
dbl: function () {
val *= 2; //这里由于前面没有添加this,也就是没有写成this.val,所以这里的val指向了全局变量
},
};
json.dbl();
console.log(json.val + val); // 12
如果将上面的题目修改成如下的形式:
var val = 1
var json = {
val: 10,
dbl: function () {
this.val *= 2 //20
}
}
json.dbl()
console.log(json.val + val)//21 20+1=21
第六,如下程序的输出结果是:
var num = 10;
var obj = { num: 20 };
obj.fn = (function (num) {
this.num = num * 3;
num++;
return function (n) {
this.num += n;
num++;
console.log(num);
};
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);
第七:this
指向call()
函数,apply()
函数,bind()
函数调用后重新绑定的对象。
我们知道通过call()
函数,apply()
函数,bind()
函数可以改变函数执行的主体,如果函数中存在this
关键字,则this
指向call()
函数,apply()
函数,bind()
函数处理后的对象。
代码如下:
//全局变量
var value = 10;
var obj = {
value: 20,
};
// 全局函数
var method = function () {
console.log(this.value);
};
method(); // 10
method.call(obj); // 20
method.apply(obj); // 20
var newMethod = method.bind(obj);
newMethod(); // 20
下面我们再来看一段代码,看一下对应的执行结果:
<body>
<button id="btn">获取用户信息</button>
<script>
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
var index = 1;
console.log(this.data[index].userName + " " + this.data[index].age);
},
};
var btn = document.getElementById("btn");
btn.onclick = userInfo.getUserInfo;
</script>
</body>
修改后的代码:
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
第八、如下程序的输出结果是:
<button id="btn">获取用户信息</button>
<script>
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
this.data.forEach(function (p) {
console.log(this);
});
},
};
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
</script>
修改后的代码:
<script>
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
var that = this;//保存this
this.data.forEach(function (p) {
console.log(that);//这里的that 指的就是当前的userInfo对象。
});
},
};
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
</script>
或者是修改成箭头函数
var userInfo = {
data: [
{ userName: "zhangsan", age: 20 },
{ userName: "lisi", age: 21 },
],
getUserInfo: function () {
// var that = this;
this.data.forEach((p) => {
console.log(this);
});
},
};
var btn = document.getElementById("btn");
// btn.onclick = userInfo.getUserInfo;
btn.onclick = userInfo.getUserInfo.bind(userInfo);
13、call()函数,apply( )函数,bind( )函数的使用与区别
在前面我们简单的说过call( )
函数,apply( )
函数,bind( )
函数,的作用。
call( )
函数,apply( )
函数,bind( )
函数,的作用都是改变this
的指向,但是在使用方式上是有一定的区别的。
下面我们分别来看一下它们各自的使用方式:
call( )
函数的基本使用
基本语法如下:
function.call(thisObj,arg1,arg2,...)
function
表示的是:需要调用的函数。
thisObj
表示:this
指向的对象,也就是this
将指向thisObj
这个参数,如果thisObj
的值为null
或者是undefined
,则this
指向的是全局对象。
arg1,arg2,..
表示:调用的函数需要的参数。
function add(a, b) {
console.log(this);
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
add.call(sub, 3, 1);// 调用add方法,但是add方法中的this指向的是sub,最终的输出结果是4
apply( )
函数的基本使用
apply()
函数的作用与call()
函数的作用是一样的,不同的是在传递参数的时候有一定的差别
语法格式如下:
function.apply(thisObj,[argsArray])
function
表示的是:需要调用的函数。
thisObj
:this
指向的对象,也就是this
将指向thisObj
这个参数,如果thisObj
的值为null
或者是undefined
,则this
指向的是全局对象。
[argsArray]
:表示的是函数需要的参数会通过数组的形式进行传递,如果传递的不是数组或者是arguments对象,会抛出异常。
function add(a, b) {
console.log(this); // 这里指向的是sub
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
add.apply(sub, [3, 1]);
bind
函数的基本使用
function.bind(thisObj,arg1,arg2,...)
通过上面语法格式,可以看出bind
函数与call
函数的参数是一样的。
不同 的是bind
函数会返回一个新的函数,可以在任何时候进行调用。
function add(a, b) {
console.log(this); // 这里指向的是sub
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
var newFun = add.bind(sub, 3, 1); //bind 返回的是一个新的函数。
newFun();//完成对add函数的调用,同时this指向了sub
三个函数的比较
通过前面对三个函数的基本使用,可以看出,它们共同点就是改变this
的指向。
不同点:
call()
函数与apply()
函数,会立即执行函数的调用,而bind
返回的是一个新的函数,可以在任何时候进行调用。
call()
函数与bind
函数的参数是一样的,而apply
函数第二个参数是一个数组或者是arguments
对象。
应用场景
这里,我们重点看一下,关于call()
函数,bind()
函数,apply()
函数的应用场景。
求数组中的最大值与最小值
var arr = [3, 6, 7, 1, 9];
console.log(Math.max.apply(null, arr));
console.log(Math.min.apply(null, arr));
将arguments
转换成数组
function fn() {
var arr = Array.prototype.slice.call(arguments);
arr.push(6);
return arr;
}
console.log(fn(1, 2));
继承的实现
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
}
function Student(name, age, gender) {
Person.call(this, name, age);
this.gender = gender;
}
var student = new Student("zhangsan", 20, "男");
console.log(
"userName=" +
student.userName +
",userAge=" +
student.userAge +
",gender=" +
student.gender
);
改变匿名函数的this
指向
首先看一下如下程序的执行结果:
var person = [
{ id: 1, userName: "zhangsan" },
{ id: 2, userName: "lisi" },
];
for (var i = 0; i < person.length; i++) {
(function (i) {
this.print = function () {
console.log(this.id);
};
this.print();
})(i);
}
具体的实现方式如下:
var person = [
{ id: 1, userName: "zhangsan" },
{ id: 2, userName: "lisi" },
];
for (var i = 0; i < person.length; i++) {
(function (i) {
this.print = function () {
console.log(this.id);
};
this.print();
}.call(person[i], i));
}
手写call、apply及bind函数
call
方法的实现
Function.prototype.myCall = function (context) {
var args = [...arguments].slice(1);
context = context || window;
context.fn = this;
var result = context.fn(...args);
return result;
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
Add.myCall(Sub, 6, 3);
apply
函数的实现
Function.prototype.myApply = function (context) {
var result = null;
context = context || window;
context.fn = this;
if (arguments[1]) {
// console.log("arguments=", arguments[1]);// arguments= (2) [6, 3]
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
return result;
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
Add.myApply(Sub, [6, 3]);
bind
函数的实现
Function.prototype.myBind = function (context) {
// 获取参数
var args = [...arguments].slice(1), // [1,5]
fn = this;
// console.log(this);//Add
return function Fn() {
// console.log(this); //Window
return fn.apply(context, args);
};
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
var newFun = Add.myBind(Sub, 1, 5);
newFun();
<script>
function add(a, b) {
console.log(this); // 这里指向的是sub
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
var newFun = add.bind(sub, 3); //bind 返回的是一个新的函数。
newFun(2); //完成对add函数的调用,同时this指向了sub
</script>
下面,我们就实现一下关于myBind
方法参数的模拟。
Function.prototype.myBind = function (context) {
// 获取参数
var args = [...arguments].slice(1),
fn = this;
// console.log(this);//Add
return function Fn() {
// console.log(this); //Window
//这里是调用bind函数的时候传递的参数,将其转换成数组
var bindArgs = Array.prototype.slice.call(arguments);
//下面完成参数的拼接
return fn.apply(context, args.concat(bindArgs));
};
};
function Add(num1, num2) {
console.log(this);
console.log(num1 + num2);
return 10;
}
function Sub(num1, num2) {
console.log(num1 - num2);
}
var newFun = Add.myBind(Sub, 1);
console.log(newFun(8));
14、回调函数有什么缺点
在JavaScript
编程过程中,我们经常会写回调函数。
我们知道在JavaScript
中函数也是一种对象,对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
例如,如下的代码示例:
const btn=document.getElementById('btn');
btn.addEventListener('click',function(event){
})
回调函数有一个比较严重的问题,就是很容易出现回调地狱的问题。也就是实现了回调函数不断的嵌套。
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
以上的代码就是典型的回调地狱的问题,这样的代码是非常不利于阅读和维护的。
所以在ES6
中提供了Promise
以及async/await
来解决地狱回调的问题。
15、 为什么函数被称为一等公民?
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。
同时函数还可以作为类的构造函数,完成对象实例的创建。所以说,这种多重身份让JavaScript
中的函数变得非常重要,所以说函数被称为一等公民。