前端常见面试题之js基础(手写深拷贝、原型和原型链、作用域和闭包)

文章目录

  • 一、变量类型和计算
    • 1. 值类型和引用类型的区别
    • 2. typeof能判断哪些类型
      • 1. 识别所有值类型
      • 2. 识别函数
      • 3. 判断是否是引用类型
    • 2. 何时使用 `===` 何时使用 `==`
    • 4. 手写深拷贝
    • 5. 类型转换
      • 1. 字符串拼接
      • 2. ==运算符
      • 3. 逻辑运算
  • 二、原型和原型链
    • 1. class 和 继承
      • 1. class基础使用
      • 2. extends继承
    • 2. class和函数的对比
    • 3. 类型判断instanceof
      • 1. 基本用法
      • 2. 判断数组
  • 三、作用域和闭包
    • 1. 作用域
    • 2. 闭包
      • 看两个题
        • 题1
        • 题2
    • 3. this

一、变量类型和计算

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运算符可以识别以下类型:

  1. “undefined” - 未定义的值
  2. “boolean” - 布尔值
  3. “number” - 数值
  4. “string” - 字符串
  5. “symbol” - 符号(ES6新增类型)
  6. “function” - 函数
  7. “object” - 对象(包括数组、日期、正则表达式等)
  8. “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 还会进行一些类型转换。例如:0nullundefined、空字符串 ""NaN 都会被转换为 false,而其他值会被转换为 true

!!0 === false
!!null === false
!!undefined === false
!!'' === false
!!NaN === false
!!false === false

二、原型和原型链

基础知识大家可以看看这篇文章:javascript原型、原型链、继承详解

思考题:

    1. 如何准确判断一个变量是不是数组
    1. 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继承

可以通过使用关键字classextends来实现类的继承。

下面是一个简单的例子,说明如何在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。由于基本数据类型不是对象,所以它不是 NumberObject 的实例,所以在使用 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. 作用域

作用域分为全局作用域和局部作用域。

  1. 全局作用域: 全局作用域是最外层的作用域,它在整个程序中都是可见的。在全局作用域中定义的变量可以在程序的任何地方被访问。
    示例:

    var globalVar = 10;
    
    function foo() {
      console.log(globalVar); // 输出10
    }
    
    foo();
    
  2. 函数作用域:函数作用域是在函数内部声明的作用域,只在函数内部可见。在函数作用域中定义的变量只在函数内部有效,并且外部无法访问。
    示例:

    function foo() {
      var localVar = 20;
      console.log(localVar); // 输出20
    }
    
    foo();
    console.log(localVar); // 报错:localVar未定义
    
  3. 块级作用域:在ES6之前,JavaScript中没有块级作用域,只有函数作用域。但是从ES6开始,增加了块级作用域的概念,即用letconst声明的变量在块级作用域内有效。
    示例:

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的取值是在函数执行的时候决定的,而不是函数定义的时候

  1. 当作为普通函数被调用时,this的取值取决于函数的调用方式。通常情况下,this会指向全局对象(在浏览器环境下是window对象),但在严格模式下指向undefined。

示例:

function greet() {
  console.log(this);
}

greet(); // 输出:window对象
  1. 使用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
  1. 当作为对象的方法被调用时,this会指向调用该方法的对象。

示例:

let person = {
  name: 'Bob',
  greet() {
    console.log(this.name);
  }
};

person.greet(); // 输出:Bob
  1. 在class方法中调用时,this会指向该类的实例。

示例:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    console.log(this.name);
  }
}

let person = new Person('Alice');
person.greet(); // 输出:Alice
  1. 在箭头函数中被调用时,this的值继承自外部作用域,与普通函数不同,箭头函数没有自己的this值。

示例:

let person = {
  name: 'Bob',
  greet: () => {
    console.log(this.name);
  }
};

person.greet(); // 输出:undefined

需要注意的是,以上只是一些常见的情况,实际的this取值会受到函数的定义方式、调用方式和是否使用严格模式等因素的影响。

你可能感兴趣的:(面试题,前端,javascript,开发语言)