一、变量类型和计算
1. 值类型和引用类型的区别
值类型包括:字符串(string)、数字(number)、布尔值(boolean)、undefined。
var name = "John"; //字符串
var age = 22; // 数字
var isStudent = true; //布尔值
var address; //undefined
引用类型包括:对象(object)、数组(array)、函数(function)和null。
var person = {name: "John", age: 22}; // 对象
var fruits = ["apple", "banana", "orange"]; // 数组
var greet = function() {console.log("Hello");} // 函数
var a = null // null 特殊的引用类型,指向空地址
二者的区别
当你将一个值类型赋给另一个变量时,会复制该值的副本。而当你将一个引用类型赋给另一个变量时,只会复制对该对象的引用。举例来说:
let a = 10; // 值类型
let b = a; // 复制 a 的值给 b
b = 20; // 修改 b 的值,不影响 a 的值
console.log(a); // 输出: 10
console.log(b); // 输出: 20
let arr1 = [1, 2, 3]; // 引用类型 - 数组
let arr2 = arr1; // 复制 arr1 的引用给 arr2
arr2.push(4); // 修改 arr2,同时也会修改 arr1
console.log(arr1); // 输出: [1, 2, 3, 4]
console.log(arr2); // 输出: [1, 2, 3, 4]
2. typeof能判断哪些类型
typeof运算符可以识别以下类型:
- “undefined” - 未定义的值
- “boolean” - 布尔值
- “number” - 数值
- “string” - 字符串
- “symbol” - 符号(ES6新增类型)
- “function” - 函数
- “object” - 对象(包括数组、日期、正则表达式等)
- “null” - 空值
需要注意的是,typeof运算符对于函数和null时的返回值可能会让人感到迷惑。typeof运算符在处理函数时返回"function",而不是"object"。在处理null时返回"object",这是JavaScript语言的早期版本设计时的错误,并且为了兼容性而保留了下来。
1. 识别所有值类型
let a; typeof a // undefined
let str='abc' typeof str // string
let n=100 typeof n // number
let b=true typeof b // boolean
let s=Symbol('s') typeof s // symbol
2. 识别函数
typeof function () {} // function
3. 判断是否是引用类型
typeof null // object
typeof [1,2] // object
typeof {name: 'zhangsan', age: 18} // object
2. 何时使用 === 何时使用 ==
除了判断
==null
时用两等,其他一律用===
const obj = { x: 100}
if( obj.a==
null ){}<==>
if( obj.a===
null || obj.a===
undefined) {}
这是因为:null == undefined // true
4. 手写深拷贝
深拷贝函数可以用来创建一个原对象的完全独立的副本,而不是仅仅复制其引用。以下是一个实现深拷贝的JavaScript函数:
function deepCopy(obj) {
if (obj == null || typeof obj !== 'object') {
return obj;
}
let copy = Array.isArray(obj) ? [] : {};
// 也可以这么写,这里就是判断引用类型是数组还是对象,想一想还有哪些方法可以区分数组和对象呢?Object.prototype.toString.call()
// if(obj instanceof Array) {
// copy = [];
// } else {
// copy = {};
// }
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
这个深拷贝函数使用递归的方式来遍历对象的每个属性,从而实现完全的复制。对于非对象类型的数据(如字符串、数字等),直接返回原来的值。对于对象类型的数据,检查属性是否是对象自身的属性,然后通过递归调用深拷贝函数来复制属性的值。最后返回复制后的对象副本。
使用示例:
let obj1 = {
name: "John",
age: 30,
hobbies: ['reading', 'running'],
address: {
city: "New York",
country: "USA"
}
};
let obj2 = deepCopy(obj1);
obj2.name = "Mike";
obj2.hobbies.push('swimming');
obj2.address.city = "Los Angeles";
console.log(obj1);
console.log(obj2);
输出结果:
{ name: 'John', age: 30, hobbies: [ 'reading', 'running' ], address: { city: 'New York', country: 'USA' } }
{ name: 'Mike', age: 30, hobbies: [ 'reading', 'running', 'swimming' ], address: { city: 'Los Angeles', country: 'USA' } }
通过深拷贝函数,obj1和obj2成为完全独立的对象,互不影响。
5. 类型转换
1. 字符串拼接
const a = 100 + 10 // 110
const b = 100 + '10' // 10010
const c = true + '10' // true10
当使用 “+” 运算符时,如果其中一个操作数是字符串,另一个操作数会被转换为字符串类型。例如:"Hello" + 42
会将数字 42 转换为字符串类型 “42”。
2. ==运算符
100 == '100' // true
0 == '' // true
0 == false // true
false == '' // true
null == undefined // true
3. 逻辑运算
在逻辑运算中,除了 &&
和 ||
运算符的短路求值,JavaScript 还会进行一些类型转换。例如:0
、null
、undefined
、空字符串 ""
或 NaN
都会被转换为 false
,而其他值会被转换为 true
。
!!0 === false
!!null === false
!!undefined === false
!!'' === false
!!NaN === false
!!false === false
二、原型和原型链
基础知识大家可以看看这篇文章:javascript原型、原型链、继承详解
思考题:
-
- 如何准确判断一个变量是不是数组
-
- class的原型本质,怎么理解
1. class 和 继承
1. class基础使用
使用class可以创建对象,并定义相关属性和方法。使用class可以更方便地组织和管理代码,使代码更具可读性和可维护性。
举例说明,假设需要创建一个表示矩形的类:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
getPerimeter() {
return 2 * (this.width + this.height);
}
}
通过上述代码创建了一个名为Rectangle的类,该类有width和height两个属性,以及getArea()和getPerimeter()两个方法。现在可以通过实例化这个类来创建矩形对象:
const rect = new Rectangle(5, 10);
console.log(rect.getArea()); // Output: 50
console.log(rect.getPerimeter()); // Output: 30
2. extends继承
可以通过使用关键字class
和extends
来实现类的继承。
下面是一个简单的例子,说明如何在JavaScript中使用class
实现继承:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(this.name + ' is eating.');
}
}
class Dog extends Animal {
bark() {
console.log(this.name + ' is barking.');
}
}
// 创建一个Animal的实例
const animal = new Animal('Animal');
animal.eat(); // 输出: Animal is eating.
// 创建一个Dog的实例
const dog = new Dog('Max');
dog.eat(); // 输出: Max is eating.
dog.bark(); // 输出: Max is barking.
在上面的例子中,我们定义了一个Animal
类,它有一个eat
方法。然后我们定义一个Dog
类,它通过extends
关键字继承自Animal
类,并且还有一个额外的bark
方法。
当我们创建Dog
类的实例时,它继承了父类Animal
的属性和方法,可以调用eat
方法并添加了自己的bark
方法。
继承允许我们在子类中重用父类的代码,并且可以在子类中添加额外的属性和方法以满足特定的需求。
2. class和函数的对比
在 JavaScript 中,每个对象都有一个原型(prototype),它是一个指向另一个对象的引用。原型可以是另一个对象的实例,也可以是 null。
函数(Function)是对象的一种特殊类型。在 JavaScript 中,函数也有一个原型,即 Function.prototype。类(Class)则是 ES6 引入的一种语法糖,用于定义对象的行为和属性的蓝图。
原型链是指对象在查找属性时,如果自身没有该属性,就会去原型对象上查找,如果原型对象也没有,就会继续向上查找,直到找到该属性或到达原型链的顶端(即 Object.prototype)。
关系:
- 在 JavaScript 中,类(Class)是通过函数来实现的。使用 class 声明的类是一种特殊的函数,使用 constructor 方法作为构造函数。
- 实例对象是类的实例,实例对象通过内部的 [[Prototype]] 属性指向构造函数的原型对象。
- 原型对象(prototype object)是通过类的 prototype 属性指定的,它包含类的共享属性和方法。
- 原型链的形成是通过对象的 [[Prototype]] 属性指向其构造函数的原型上的另一个对象,这样就可以在对象上查找属性时,实现链式查找。
举例对比说明:
// 以类的方式实现
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person = new Person('Alice');
person.greet(); // 输出: Hello, my name is Alice
// 以函数的方式实现
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person = new Person('Bob');
person.greet(); // 输出: Hello, my name is Bob
在这个对比示例中,class 和函数的方式实现了同样的功能,创建了一个 Person
对象,然后通过 greet
方法打印问候语。
-
class
的方式更加直观和易读,它隐藏了原型相关的细节,更符合传统的面向对象语言的写法。 - 函数的方式更加灵活,更容易理解对象、函数和原型之间的联系。实际上,
class
只是function
的语法糖。 - 无论是 class 还是函数的方式,它们的实例都共享通过原型对象定义的方法,通过原型链实现了属性和方法的继承关系。
3. 类型判断instanceof
1. 基本用法
JavaScript 中的 instanceof 运算符用于检查一个对象是否是另一个对象的实例。
它的语法是 object instanceof constructor
,其中 object
是要检查的对象,constructor
是要比较的构造函数。
实例如下:
function Person(name) {
this.name = name;
}
var person1 = new Person("Alice");
console.log(person1 instanceof Person); // 输出 true
console.log(person1 instanceof Object); // 由于所有对象都是 Object 的实例,所以也输出 true
var num = 10;
console.log(num instanceof Number); // 输出 false,因为 num 是基本数据类型而不是对象
console.log(num instanceof Object); // 输出 false
在上面的示例中,我们定义了一个构造函数 Person
,并创建了一个 person1
的实例。通过使用 instanceof 运算符,我们可以检查 person1
是否是 Person
的实例,以及它是否是 Object
的实例。
另外,在第二个示例中,我们创建了一个基本数据类型的变量 num
。由于基本数据类型不是对象,所以它不是 Number
或 Object
的实例,所以在使用 instanceof 运算符时会返回 false。
2. 判断数组
下面是一个使用instanceof来判断一个引用类型是否是数组的例子:
var arr = [1, 2, 3];
console.log(arr instanceof Array); // true
var obj = { name: 'John', age: 25 };
console.log(obj instanceof Array); // false
在上面的例子中,我们定义了一个数组arr和一个对象obj。使用instanceof运算符,我们可以确定arr是一个Array类型的实例,而obj不是。这是因为Array是JavaScript中的一个内置对象类型,而obj是个普通的对象。
需要注意的是,instanceof运算符只能判断对象是否是某个特定构造函数类型的实例,无法判断对象是否是任意的数组类型。如果涉及多个不同的数组实例,最好使用Array.isArray()方法来进行判断。
var arr1 = [1, 2, 3];
var arr2 = new Array(4, 5, 6);
console.log(Array.isArray(arr1)); // true
console.log(Array.isArray(arr2)); // true
通过Array.isArray()方法,我们可以直接判断一个变量是否为数组类型,而无需使用instanceof判断。
三、作用域和闭包
1. 作用域
作用域分为全局作用域和局部作用域。
-
全局作用域: 全局作用域是最外层的作用域,它在整个程序中都是可见的。在全局作用域中定义的变量可以在程序的任何地方被访问。
示例:var globalVar = 10; function foo() { console.log(globalVar); // 输出10 } foo();
-
函数作用域:函数作用域是在函数内部声明的作用域,只在函数内部可见。在函数作用域中定义的变量只在函数内部有效,并且外部无法访问。
示例:function foo() { var localVar = 20; console.log(localVar); // 输出20 } foo(); console.log(localVar); // 报错:localVar未定义
-
块级作用域:在ES6之前,JavaScript中没有块级作用域,只有函数作用域。但是从ES6开始,增加了块级作用域的概念,即用
let
和const
声明的变量在块级作用域内有效。
示例:
function foo() {
if (true) {
let blockVar = 30;
console.log(blockVar); // 输出30
}
console.log(blockVar); // 报错:blockVar未定义
}
foo();
在上面的代码中,blockVar
只在if语句的块级作用域内有效,无法在块级作用域外访问。
2. 闭包
闭包是指在JavaScript中,一个函数可以访问其外部函数作用域中的变量,即使该外部函数已经调用结束或者返回,依然可以访问到这些变量的现象。
在JavaScript中,当一个函数内部定义的函数引用了外部函数的变量,就会形成闭包
。闭包内部的函数可以访问其外部函数的变量,而外部函数不能访问内部函数的变量。这是因为JavaScript的作用域链机制
,内部函数在访问变量时,会先从自身的作用域查找,若没有找到,则会继续向上一级作用域查找,直到找到为止。这种机制导致了内部函数可以访问到外部函数的变量。
以下是一个闭包的例子:
function outerFunction() {
var outerVariable = 'Hello';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
var closure = outerFunction();
closure(); // 输出 'Hello'
在上面的例子中,innerFunction被定义在outerFunction内部,并且引用了outerFunction的变量outerVariable。当outerFunction执行完毕后,返回innerFunction,但innerFunction仍然可以访问outerVariable。这就是闭包的表现,即使outerFunction已经执行完毕,outerVariable仍然存在于内存中被innerFunction所引用。
看两个题
题1
function fn() {
var a = 100;
return function() {
console.log(a);
}
}
var a = 200;
var f = fn();
f();
这个题最后打印输出是什么?
答案是:
100
因为函数f是由函数fn返回的一个闭包,闭包包含了fn的环境变量和fn中定义的内部函数。在调用f函数时,它会访问和使用fn中的环境变量a,而不是全局变量a。在fn函数中,a被赋值为100, 所以最后输出的是100。
题2
function f(fn) {
const a = 100;
fn();
}
const a = 200;
function fn2() {
console.log(a); // undefined
}
f(fn2);
这个题最后打印输出是什么?
答案是:
200
这段程序最后会输出200。原因是在函数f中调用了参数fn,相当于在函数内部执行了fn2函数。在执行fn2函数时,会从函数定义的作用域中寻找变量a
,而当前作用域中有一个变量a的值为200,所以输出的是200。
总结:闭包中自由变量的查找,是在函数定义的地方向上级作用域查找,而不是执行的地方
3. this
this的取值是在函数执行的时候决定的,而不是函数定义的时候
- 当作为普通函数被调用时,this的取值取决于函数的调用方式。通常情况下,this会指向全局对象(在浏览器环境下是window对象),但在严格模式下指向undefined。
示例:
function greet() {
console.log(this);
}
greet(); // 输出:window对象
- 使用call、apply或bind方法进行调用时,可以手动指定this的值。
示例:
function greet() {
console.log(this.name);
}
let person = {
name: 'Bob'
};
greet.call(person); // 输出:Bob
greet.apply(person); // 输出:Bob
let greetPerson = greet.bind(person);
greetPerson(); // 输出:Bob
- 当作为对象的方法被调用时,this会指向调用该方法的对象。
示例:
let person = {
name: 'Bob',
greet() {
console.log(this.name);
}
};
person.greet(); // 输出:Bob
- 在class方法中调用时,this会指向该类的实例。
示例:
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(this.name);
}
}
let person = new Person('Alice');
person.greet(); // 输出:Alice
- 在箭头函数中被调用时,this的值继承自外部作用域,与普通函数不同,箭头函数没有自己的this值。
示例:文章来源:https://www.toymoban.com/news/detail-813801.html
let person = {
name: 'Bob',
greet: () => {
console.log(this.name);
}
};
person.greet(); // 输出:undefined
需要注意的是,以上只是一些常见的情况,实际的this取值会受到函数的定义方式、调用方式和是否使用严格模式等因素的影响。文章来源地址https://www.toymoban.com/news/detail-813801.html
到了这里,关于前端常见面试题之js基础(手写深拷贝、原型和原型链、作用域和闭包)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!