1.JavaScript中的数据类型有哪几种?并写出判断数据类型的方法
JavaScript中的数据类型有以下几种:
-
基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6新增)
-
引用数据类型:Object、Array、Function、Date、RegExp 等
判断数据类型的方法有以下几种:
-
typeof 操作符:可以返回一个字符串,表示操作数的数据类型。例如:typeof 123 返回 “number”,typeof “hello” 返回 “string”。
-
instanceof 操作符:用于判断某个对象是否属于某个类。例如:[1, 2, 3] instanceof Array 返回 true。
-
Object.prototype.toString.call() 方法:可以返回一个表示对象类型的字符串。例如:Object.prototype.toString.call([1, 2, 3]) 返回 “[object Array]”。
-
constructor 属性:可以返回创建某个对象的构造函数。例如:(123).constructor 返回 Number 构造函数。
2.简述一下let,var,const的区别;
let,var和const是JavaScript中用于声明变量的关键字,它们之间有以下区别:
-
作用域:var声明的变量具有函数作用域,即在声明的函数内部有效,而在函数外部也可以访问。let和const声明的变量具有块级作用域,即在声明的块(如{})内部有效,而在块外部无法访问。
-
变量提升:var声明的变量存在变量提升,即可以在声明之前访问变量。let和const声明的变量不存在变量提升,即在声明之前访问变量会抛出错误。
-
重复声明:var可以重复声明同一个变量,而let和const在同一作用域内重复声明同一个变量会抛出错误。
-
初始化:var声明的变量在声明时可以不进行初始化,而let和const声明的变量必须进行初始化。
-
可变性:var和let声明的变量可以被重新赋值,而const声明的变量是常量,不可被重新赋值。
综上所述,推荐使用let和const来声明变量,let用于声明需要修改的变量,const用于声明常量。var在ES6之后已经不推荐使用,因为它存在一些问题,如变量提升和作用域问题。
3.JavaScript中call,apply,bind的区别和用法
call、apply和bind都是用于改变函数的执行上下文(即函数内部的this指向)的方法,它们之间的区别和用法如下:
-
call:使用call方法可以调用一个函数,并且指定函数内部的this指向。除了第一个参数是要指定的this值外,后续参数是要传递给函数的参数,可以是多个参数,用逗号分隔。例如:func.call(obj, arg1, arg2)。
-
apply:使用apply方法也可以调用一个函数,并且指定函数内部的this指向。与call方法不同的是,apply方法的第二个参数是一个数组或类数组对象,数组中的元素将作为参数传递给函数。例如:func.apply(obj, [arg1, arg2])。
-
bind:使用bind方法可以创建一个新的函数,并且指定函数内部的this指向。bind方法不会立即执行函数,而是返回一个新的函数,需要手动调用才会执行。与call和apply不同的是,bind方法可以预先指定函数的部分参数,这在某些场景下非常有用。例如:var newFunc = func.bind(obj, arg1, arg2)。
总结:
- call和apply可以立即执行函数,而bind返回一个新的函数需要手动调用。
- call和apply的参数传递方式不同,call的参数是逐个传递,apply的参数是数组或类数组对象。
- bind可以预先指定函数的部分参数,并返回一个新的函数。
这些方法的主要用途是在特定的上下文中调用函数,常用于改变函数内部的this指向,或者在某个对象上调用原本不属于该对象的方法。
4.JavaScript对象创建的方式有哪些?
JavaScript对象可以通过以下几种方式进行创建:
- 对象字面量(Object Literal):使用花括号 {} 来创建一个对象,并在花括号中定义对象的属性和方法。例如:
var obj = {
name: "John",
age: 25,
sayHello: function() {
console.log("Hello!");
}
};
- 构造函数(Constructor):使用构造函数创建对象,通过使用 new 关键字和构造函数来实例化一个对象。例如:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log("Hello!");
};
}
var obj = new Person("John", 25);
- Object.create() 方法:使用 Object.create() 方法可以创建一个新对象,并将新对象的原型设置为指定的对象。例如:
var person = {
name: "John",
age: 25,
sayHello: function() {
console.log("Hello!");
}
};
var obj = Object.create(person);
- ES6的类(Class):使用 class 关键字可以定义一个类,通过实例化该类来创建对象。例如:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello!");
}
}
var obj = new Person("John", 25);
这些是常用的创建对象的方式,每种方式都有其适用的场景,可以根据具体需求选择合适的方式来创建对象。
5.简述一下浏览器的垃圾回收机制
浏览器的垃圾回收机制是指浏览器对不再使用的内存进行自动回收和释放的过程。垃圾回收机制的主要目的是优化内存的使用,防止内存泄漏和过度占用。
浏览器的垃圾回收机制主要有两种方式:标记清除和引用计数。
-
标记清除:标记清除是一种常用的垃圾回收算法。它的基本思想是通过标记活动对象,然后清除未被标记的对象。垃圾回收器会从根对象开始,标记所有与根对象相关联的对象,然后遍历整个对象图,标记所有可达的对象。最后,清除没有被标记的对象,释放它们占用的内存空间。
-
引用计数:引用计数是另一种常见的垃圾回收算法。它的基本思想是为每个对象维护一个引用计数器,记录对象被引用的次数。当对象的引用计数器变为0时,说明该对象不再被使用,可以被回收。然而,引用计数算法无法解决循环引用的问题,即两个或多个对象相互引用,导致它们的引用计数器都不为0,但实际上它们已经不可达。为了解决这个问题,现代浏览器通常会结合标记清除算法来处理循环引用。
除了这两种常见的垃圾回收机制,浏览器还会使用一些优化技术来提高垃圾回收的效率,例如增量标记、延迟回收等。增量标记可以将垃圾回收的过程分成多个阶段,每次执行一小部分,减少对页面渲染的影响。延迟回收则是将垃圾回收的时间点延迟到系统空闲时进行,避免影响用户的操作体验。
总之,浏览器的垃圾回收机制是为了优化内存的使用,自动回收和释放不再使用的内存。不同的浏览器可能采用不同的垃圾回收算法和优化技术,但基本目标都是相同的。
6.New操作符执行过程
当使用new
操作符创建一个对象时,以下是new
操作符的执行过程:
-
创建一个空对象:
new
操作符会创建一个空对象,该对象继承自构造函数的原型对象。 -
设置对象的原型链:新创建的空对象的
__proto__
属性会指向构造函数的原型对象,以建立对象与构造函数之间的原型链关系。 -
执行构造函数:将新创建的对象作为
this
关键字的值传递给构造函数,并执行构造函数内的代码。在构造函数内部,可以通过this
关键字来操作和设置新对象的属性和方法。 -
返回新对象:如果构造函数没有显式返回一个对象,则
new
操作符会隐式返回新创建的对象。如果构造函数返回一个非对象类型的值(例如基本类型),则新对象将被忽略,仍然返回新创建的对象。
例如,假设有一个构造函数Person
:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log("Hello, " + this.name);
}
var person = new Person("Alice", 25);
person.sayHello(); // 输出:Hello, Alice
在上述示例中,使用new
操作符创建了一个Person
对象。new Person("Alice", 25)
会创建一个新的空对象,并将其作为this
关键字的值传递给Person
构造函数。在构造函数内部,将name
和age
属性设置为传入的参数值。最后,new
操作符隐式返回新创建的对象,将其赋值给person
变量。
7.已知如下数组,编写一个程序将数组扁平化去并除其中重复部分数据,最终得 到一个不重复的数组
如var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]
化为:arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
可以使用递归和Set数据结构来实现数组的扁平化和去重操作。以下是一个示例代码:
function flattenAndRemoveDuplicates(arr) {
var result = [];
function flatten(arr) {
for (var i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
flatten(arr[i]);
} else {
result.push(arr[i]);
}
}
}
flatten(arr);
return Array.from(new Set(result));
}
var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
var flattenedArr = flattenAndRemoveDuplicates(arr);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
在上述代码中,flattenAndRemoveDuplicates
函数使用递归来将多维数组扁平化为一维数组,并使用 Set 数据结构来去除重复元素。最后,通过 Array.from
方法将 Set 转换为数组,并返回最终结果。
8.
let a =1
function bar(){
let b = () => a;
a =2;
return b;
}
console.log(bar()());
在给定的代码中,首先定义了一个变量 a
并赋值为 1
。然后定义了一个函数 bar
,在函数内部又定义了一个箭头函数 b
,箭头函数 b
返回了变量 a
的值。接着,将变量 a
的值修改为 2
。最后,通过 console.log(bar()())
打印了 bar
函数的返回值,即箭头函数 b
的执行结果。
根据代码的执行顺序,可以得出以下结果:
- 执行
bar()
,箭头函数b
被返回,并且在返回之前,将变量a
的值修改为2
。 - 执行
bar()()
,即执行返回的箭头函数b
,返回变量a
的值。由于此时变量a
的值已经被修改为2
,因此输出结果为2
。
所以,最终的输出结果是 2
。
9.
console.log(1);
setTimeout(function() {
console.log(2);},10)
setTimeout(function() {
console.log(3);},0)
console.log(4);
new Promise(function(resolve){
console.log(5); resolve( )
}).then(function() {
console.log(6);
console.log(7);
在给定的代码中,首先执行了 console.log(1)
,输出结果为 1
。然后执行了一个 10 毫秒的定时器,定时器的回调函数输出结果为 2
。接着执行了一个 0 毫秒的定时器,定时器的回调函数输出结果为 3
。然后执行了 console.log(4)
,输出结果为 4
。接下来执行了一个 Promise,输出结果为 5
。Promise 的 then
方法中的回调函数输出结果为 6
和 7
。所以最终的输出结果为:
1
4
5
6
7
3
2
其中,5
、6
和 7
的输出顺序可能会发生变化,但是它们的输出顺序一定在 3
和 2
之前。这是因为 Promise 的 then
方法中的回调函数会被放入微任务队列中,等待当前任务执行完毕后执行。而定时器的回调函数会被放入宏任务队列中,需要等待当前任务执行完毕后,等待一定的时间后才会执行。因此,微任务的执行顺序一定在宏任务之前。
10.
var name = 'window'
var person1 ={ name: 'person1',
foo1: function () {
console.log(this.name)},
foo2: () => console.log(this.name), foo3: function (){ return function () { console.log(this.name )
foo4: function (){ return () => {
console.log(this.name)
var person2 = name: 'person2'}
// 开始题目: person1.foo1( ); person1.foo2( ); person1.foo3( )(); person1.foo4()();
person1.foo2.call(person2)
在给定的代码中,首先定义了一个全局变量 name
并赋值为 'window'
。然后定义了一个对象 person1
,其中包含了四个方法 foo1
、foo2
、foo3
和 foo4
。接着定义了一个对象 person2
,其中包含了一个属性 name
并赋值为 'person2'
。
根据代码的执行顺序,可以得出以下结果:
person1.foo1()
:调用foo1
方法,输出结果为'person1'
。这是因为foo1
方法是一个普通函数,它的执行上下文中的this
指向调用它的对象person1
。person1.foo2()
:调用foo2
方法,输出结果为'window'
。这是因为foo2
方法是一个箭头函数,箭头函数的this
绑定是在定义时确定的,而不是在运行时确定的。在定义时,箭头函数的this
绑定了外层作用域的this
,即全局对象window
。person1.foo3()()
:调用foo3
方法并立即调用返回的函数,输出结果为'window'
。这是因为foo3
方法返回的是一个普通函数,而不是箭头函数。在调用返回的函数时,它的执行上下文中的this
指向全局对象window
。person1.foo4()()
:调用foo4
方法并立即调用返回的函数,输出结果为'person1'
。这是因为foo4
方法返回的是一个箭头函数,箭头函数的this
绑定了外层作用域的this
,即foo4
方法的执行上下文中的this
,而foo4
方法是在对象person1
的上下文中调用的,所以箭头函数的this
指向对象person1
。person1.foo2.call(person2)
:使用call
方法将foo2
方法中的this
绑定到对象person2
上,输出结果为'window'
。这是因为箭头函数的this
绑定是在定义时确定的,无法通过call
、apply
或bind
方法来改变。
所以最终的输出结果为:
person1
window
window
person1
window
11.
var functionArray =[];
for(var i = 0;i < 10;i++){
functionArray.push(
function(){
console.log(i);
for(var i = 0;i < 10;i++){
functionArray[i]();
functionArray[5]();
这段代码定义了一个空数组 functionArray
,并使用一个循环向数组中添加了10个函数。每个函数都会输出变量 i
的值。
然后,在循环结束后,又定义了一个新的循环。在这个新的循环中,每次循环都会调用数组中的函数,并再次调用索引为5的函数。
由于 JavaScript 中的变量作用域是函数作用域,而不是块级作用域,所以在第一个循环中定义的变量 i
在整个循环结束后仍然存在,并且其值为10。因此,当调用数组中的函数时,它们都会输出10。
同时,在第二个循环中,每次循环都会调用索引为5的函数。由于这个函数是在第一个循环中定义的,所以它的执行上下文中的变量 i
也是10。因此,无论第二个循环执行多少次,调用索引为5的函数都会输出10。
因此,最终的输出结果将是10。