记录JavaScript基础知识和代码练习

140 阅读23分钟

记录所有javascript基础知识和代码练习

当你声明 let a = 1; 在JavaScript中,a 是一个基本数据类型的实例,具体来说是一个数字(Number)。在JavaScript中,基本数据类型包括数字(Number)、字符串(String)、布尔值(Boolean)、null、undefined、Symbol(ES6中新增),以及ES2020中新增的BigInt。

基本数据类型与对象是不同的。基本数据类型是不可变的,并且它们具有属性和方法。例如,当你对一个数字使用方法时,JavaScript 会临时将这个数字包装成一个对象,以便可以使用方法或属性,然后再还原回基本类型。这种行为是自动的,对于编程者来说通常是透明的。

举个例子,当你对一个数字使用 .toString() 方法时,像这样:

let a = 1;
console.log(a.toString()); // "1"

JavaScript暂时将 a 转换成一个 Number 对象,这个对象具有 toString 方法,然后再将结果转换回基本数据类型。

总之,虽然JavaScript在某些情况下会自动将基本数据类型转换为对象,但 let a = 1; 这种情况下,a 本身是一个基本数据类型的实例,而不是一个对象。


在JavaScript中,术语 "方法"(method)和 "函数"(function)经常被提及,它们之间有紧密的联系但也有一些区别:

  1. 函数 (Function): 在JavaScript中,函数是一段可以被重复调用的代码块。函数可以独立存在,也可以作为对象的一部分。函数是JavaScript的基础组成部分,用于执行特定的任务或计算值。函数可以接受输入(称为参数),并可以返回一个值。

    例如,一个简单的函数定义如下:

    function greet() {
        console.log("Hello!");
    }
    
  2. 方法 (Method): 方法实际上是附加在对象上的一种特殊类型的函数。当一个函数作为对象的属性时,我们称之为该对象的方法。方法可以访问和操作对象的数据。

    例如,一个对象及其方法的定义如下:

    let person = {
        name: "Alice",
        greet: function() {
            console.log("Hello, my name is " + this.name);
        }
    };
    

    在这个例子中,greetperson 对象的一个方法。

关系和区别总结如下:

  • 关系:所有的方法都是函数,但并非所有的函数都是方法。方法是定义为对象属性的函数。
  • 上下文 (this) 的不同:方法通常与其所属的对象相关联,并且在方法内部,this 关键字通常指向调用它的对象。而独立的函数没有这样的关联,除非明确地通过某些方法(如 .call(), .apply(), 或 .bind())设置 this 值。
  • 调用方式:方法是通过其所属对象来调用的,例如 person.greet()。而函数可以直接调用,例如 greet()

在JavaScript中,function是一种特殊的对象类型。这意味着函数实际上是JavaScript中的一种对象,它们可以像任何其他对象一样具有属性和方法。这也是JavaScript的函数具有一些独特特性的原因,例如:

  1. 一级函数(First-Class Functions):在JavaScript中,函数被视为一级公民。这意味着函数可以被存储在变量中,可以作为参数传递给其他函数,也可以作为其他函数的返回值。

  2. 函数表达式(Function Expressions):JavaScript允许创建匿名函数(没有名称的函数),这些函数可以被分配给变量或作为其他函数的返回值。

  3. 闭包(Closures):JavaScript函数能够记住并访问它们被定义时的作用域,即使它们在当前作用域之外执行,这是通过闭包机制实现的。

  4. 方法(Methods):由于函数是对象,所以它们可以作为对象的属性。当函数作为对象的属性时,它们通常被称为该对象的方法。

  5. 构造函数(Constructor Functions):在JavaScript中,可以使用函数作为构造器来创建新对象。通过使用new关键字,可以调用函数来创建具有指定原型的新对象。

因此,在JavaScript中,函数不仅仅是代码的集合,它们也是可以操作的对象,使得JavaScript非常灵活和功能强大。


在JavaScript中,nullNaNundefined 都代表不同的概念,它们分别用于表示不同类型的“无”或“空”值。以下是它们各自的含义和区别:

  1. null

    • null 表示“没有值”或“空值”。它是一个故意分配给变量的值,表示该变量不指向任何对象或值。
    • null 是一个特殊的字面量值,不是一个对象或者对象的引用,尽管 typeof null 错误地返回 "object"
    • 在比较时,null == undefined 为真,但 null === undefined 为假,因为 === 比较同时包含值和类型。
  2. NaN

    • NaN 是"Not a Number"的缩写,表示一个非数值。
    • 它是一种特殊的浮点数值,用于表示无法返回正常结果的数学运算的结果,比如 0/0sqrt(-1)
    • NaN 在JavaScript中是唯一一个不等于自身的值,即 NaN === NaNNaN == NaN 都返回假(false)。因此,判断一个值是否为 NaN 通常使用 isNaN() 函数。
    • NaN 的类型是 "number",即 typeof NaN 返回 "number"
  3. undefined

    • undefined 表示变量已声明但尚未被赋值,或者对象属性或数组元素不存在。
    • 它是变量的默认初始值。如果函数没有明确返回值,则默认返回 undefined
    • undefined 是一个类型为 "undefined" 的特殊值,即 typeof undefined 返回 "undefined"
    • null 一样,undefined == null 为真,但 undefined === null 为假。

总结来说,null 是一个表示“无”的值,NaN 是一个表示非数值的特殊数值,而 undefined 表示变量未被赋值。在实际编程中,理解这些值的不同含义对于编写清晰、准确的代码非常重要。


在JavaScript中,对象是非常核心的概念。对象可以被理解为一个包含属性和方法的集合。这里是对象在JavaScript中的一些关键特性和用法:

  1. 属性和方法

    • 属性:对象的属性是它所包含的数据。每个属性都有一个键(通常是字符串)和一个值,这个值可以是任何JavaScript数据类型,甚至是另一个对象。
    • 方法:对象的方法是定义在对象上的函数。方法可以操作对象的内部数据,也可以完成其他任务。
  2. 对象字面量

    • 对象可以通过对象字面量的方式创建,这是一种简洁的定义对象及其属性和方法的语法。
    const person = {
      name: "Alice",
      age: 25,
      greet: function() {
        console.log("Hello!");
      }
    };
    
  3. 构造函数和类

    • 除了对象字面量,对象也可以通过构造函数或类(ES6引入的语法糖)来创建。这对于创建多个具有相同结构的对象特别有用。
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    Person.prototype.greet = function() {
      console.log("Hello!");
    };
    
    const alice = new Person("Alice", 25);
    
  4. 原型和继承

    • 在JavaScript中,对象之间的继承是通过原型链实现的。每个对象都有一个原型对象,对象从其原型继承属性和方法。
  5. this关键字

    • 在方法内部,this 关键字指向调用该方法的对象。
  6. 动态性

    • JavaScript的对象是动态的,这意味着在创建后可以随时添加或删除属性和方法。
  7. 内置对象

    • JavaScript还提供了多种内置对象,如 Array, Date, Math, 和 String 等,这些都提供了各自的属性和方法。
  8. JSON(JavaScript Object Notation)

    • JSON是一种基于JavaScript对象字面量语法的数据格式,广泛用于数据交换。

在JavaScript中,判断一个值是否是对象可以通过几种不同的方法来实现。以下是一些常见的方式:

  1. 使用 typeof 操作符

    • typeof 操作符可以用来检测一个值的类型。但要注意,对于对象(包括数组和函数),typeof 都会返回 "object"。因此,这种方法需要与其他检测方法结合使用。
    • 例如,typeof null 也会返回 "object",这是JavaScript的一个已知错误。
    if (typeof value === 'object' && value !== null) {
      // value 可能是对象
    }
    
  2. 使用 instanceof 操作符

    • instanceof 操作符用于检测构造函数的 prototype 属性是否存在于某个实例对象的原型链上。
    • 这个方法对于普通的对象(由 {}new Object 创建)是有效的。
    if (value instanceof Object) {
      // value 是一个对象
    }
    
  3. 使用 Object.prototype.toString 方法

    • 这是一个更准确的方法,可以用来检测一个值是否是普通对象。它避免了 typeof null 返回 "object" 的问题。
    if (Object.prototype.toString.call(value) === '[object Object]') {
      // value 是一个普通对象
    }
    
  4. 检查对象构造器

    • 如果你想检查一个对象是否是通过特定的构造函数创建的,你可以直接检查其 constructor 属性。
    if (value && value.constructor === Object) {
      // value 是通过 Object 构造函数创建的
    }
    

在使用这些方法时,需要根据具体情况和需要判断的对象类型来选择最合适的方法。例如,如果你需要区分数组、函数和普通对象,可能需要组合使用几种方法来进行准确判断。


Function

  • 函数声明(Function Declaration)
const v=myf(1,99)
function myf(a,b){
    return a*b
}
console.log(v)  
  • 函数表达式(Function Expression)
const myf = function(a,b){ 
    return a+b
}
  • 箭头函数(Arrow Function)
const myf2 = (a,b) => a+b;  //箭头定义
console.log(myf2(4,5)===myf(5,4))
  1. 使用普通函数(Standard Function): 这是传统的函数表达方式,使用 function 关键字。

    const names = ['Alice', 'Bob', 'Carol'];
    names.forEach(function(n) {
      console.log(`Hello ${n}`);
    });
    

    在这里,function(n) 是一个匿名函数,传递给 forEach 方法。这个函数接收数组的每个元素作为参数 n,然后执行 console.log

  2. 使用箭头函数(Arrow Function): 箭头函数提供了一种更简洁的函数声明方式。

    const names = ['Alice', 'Bob', 'Carol'];
    names.forEach((n) => {
      console.log(`Hello ${n}`);
    });
    

    这里的 (n) => { console.log(Hello ${n}); } 是一个箭头函数,作为 forEach 方法的参数。它同样接收数组的每个元素作为参数 n

  3. 函数表达式(Function Expression): 在这个例子中,您定义了一个命名函数 fooo,然后将其作为参数传递给 forEach 方法。

    function fooo(n) {
      console.log(`Hello ${n}`);
    }
    
    const names = ['Alice', 'Bob', 'Carol'];
    names.forEach(fooo);
    

    这里,fooo 是一个独立定义的函数,接收一个参数 n。然后在 forEach 方法中,我们直接使用函数名 fooo 来引用它。

在所有这些情况下,每种方法都有效地遍历了 names 数组,并对每个元素执行了相同的操作,即打印出一条问候语。选择哪种方法主要取决于个人偏好和代码的上下文。


loop:for loop 和 while loop

console.log('loop presentation ---------->>>')
const arr=[1,31]
for (let i=0; i < arr.length; i++) {
    console.log(arr[i]);
}
console.log('--------------------while loop')
var i = 0;
while (i < arr.length) {
    console.log(arr[i]);
    i++;
}

switch用法

--------------SWITCH------------------
console.log('switch expression presentation ---------->>\n')
let fruit = "apple";
switch (fruit) {
  case "apple":
    console.log("Apple is $0.50 per pound.");
    break;
  case "banana":
    console.log("Banana is $0.70 per pound.");
    break;
  default:
    console.log("Sorry, we are out of " + fruit + ".");
}

###变量作用域 块级作用域是一个在现代JavaScript(ECMAScript 2015或ES6及以后版本)中引入的重要概念,它与传统的函数作用域形成对比。在块级作用域中,变量(使用letconst声明)在它被声明的块或子块中是有效的,但在外部则不可访问。一个“块”通常是由花括号 {} 包围的一段代码。

在 JavaScript 中,当 ij 是在函数 h 内部定义的,它就是一个局部变量,这确实涉及到变量的作用域。在 JavaScript 中,变量的作用域决定了代码的哪些部分可以访问这个变量。

const h = function(x) {
    const ij = "composing";
    console.log(x);
    return null;
};

ij 被定义为局部变量,这意味着它只能在定义它的函数 h 的内部被访问。这是因为 ij 遵循块级作用域(由于使用了 const 声明),这是 JavaScript 中的一个基本原则。

  1. 局部作用域(函数作用域或块级作用域): 局部作用域指的是变量只能在它被声明的函数或代码块内部访问。在 h 函数内声明的 ij 变量就是一个局部变量。它不能在函数 h 的外部访问,因为它的作用域被限制在了 h 函数的内部。

  2. 全局作用域: 如果一个变量在任何函数或代码块之外声明,那么它就有全局作用域,这意味着它可以在代码的任何地方被访问。

  3. 块级作用域: 使用 letconst 声明的变量具有块级作用域(例如在 if 语句、循环或任何大括号 {} 内声明的变量)。这表示这些变量只能在它们被声明的特定代码块内访问。 解构赋值(Destructuring Assignment)是JavaScript中的一种语法特性,它允许你直接从数组或对象中提取值,并将它们赋给变量,使得代码更简洁易读。这个特性在ECMAScript 2015 (也称为ES6)中被引入。

数组解构

对于数组,解构赋值可以让你直接从数组中提取元素,并创建具有相同值的变量。

let [a, b, c] = [1, 2, 3];
console.log(a); // 输出 1
console.log(b); // 输出 2
console.log(c); // 输出 3

你还可以跳过某些元素,或者使用剩余运算符来提取其余元素:

let [a, , c] = [1, 2, 3];
console.log(a); // 输出 1
console.log(c); // 输出 3

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 输出 1
console.log(rest);  // 输出 [2, 3, 4]

对象解构

对于对象,解构赋值可以让你直接从对象属性中提取值,并创建同名变量。

let {x, y} = {x: 1, y: 2};
console.log(x); // 输出 1
console.log(y); // 输出 2

如果你想将属性赋值给不同名称的变量,可以这样做:

let {x: a, y: b} = {x: 1, y: 2};
console.log(a); // 输出 1
console.log(b); // 输出 2

块级作用域的特点

  1. 局限于块:使用letconst声明的变量在包含它们的块(如if语句、for循环、while循环等)内部是有效的,但在外部是不可见的。

    if (true) {
        let blockScopedVariable = 'visible';
    }
    console.log(blockScopedVariable); // 报错:blockScopedVariable is not defined
    
  2. 循环中的作用域:在循环中使用letconst声明变量时,每次迭代都会创建该变量的新实例。

    for (let i = 0; i < 3; i++) {
        console.log(i); // 依次输出 0, 1, 2
    }
    console.log(i); // 报错:i is not defined
    
  3. 防止变量提升letconst声明的变量不会提升到它们所在作用域的顶部,与使用var声明的变量不同。

    console.log(a); // 报错:Cannot access 'a' before initialization
    let a = 3;
    
  4. 暂时性死区:在代码块内,letconst声明的变量在声明之前都是不可访问的,这被称为暂时性死区(Temporal Dead Zone, TDZ)。

    if (true) {
        console.log(a); // 报错:Cannot access 'a' before initialization
        let a = 3;
    }
    
  5. 重复声明的限制:在同一作用域或块中,不能用letconst重复声明同一个变量。

    let a = 1;
    let a = 2; // 报错:Identifier 'a' has already been declared
    

在JavaScript中,变量提升(Hoisting)是一种行为,它将变量和函数声明在代码执行之前移至它们各自作用域的顶部。这意味着无论声明在代码中的位置如何,变量和函数声明在执行任何代码之前都已经可用。这是JavaScript的一个独特特性,对于理解如何组织和使用变量非常重要。

变量提升的行为

  1. 变量声明被提升:使用var关键字声明的变量会被提升到它们所在的函数作用域或全局作用域的顶部。但是,它们的初始化值不会被提升。

    console.log(x); // 输出 undefined,而不是报错
    var x = 5;
    console.log(x); // 输出 5
    

    在上面的例子中,变量x的声明被提升到作用域的顶部,但是它的赋值5留在原处。

  2. 函数声明被提升:函数声明(而非表达式)会被整体提升到它们所在作用域的顶部。

    hello(); // 输出 "Hello, world!"
    
    function hello() {
        console.log("Hello, world!");
    }
    

    在这个例子中,即使函数调用发生在函数声明之前,它也能正常工作,因为函数声明被提升。

letconst与变量提升

var不同,使用letconst声明的变量也会被提升,但它们不会被初始化。这意味着它们在声明之前存在于一个“暂时性死区”(Temporal Dead Zone, TDZ),在这个区域内访问这些变量会导致错误。

console.log(y); // 报错:Cannot access 'y' before initialization
let y = 10;

在这个例子中,变量y是在使用let声明的,它在声明之前不能被访问。


闭包函数分析

function stringAccumulator(initialValue) {
    let val = initialValue;
    return function(s) {
      val += '\n' + s;
      console.log(val);
    };
  }
  
  const addColdStates = stringAccumulator('Cold states:');
  
  // 调用闭包函数,并传递参数
  addColdStates('Minnesota'); // 输出 "Cold states:\nMinnesota"
  addColdStates('Maine');     // 输出 "Cold states:\nMinnesota\nMaine"
  addColdStates('Michigan');  // 输出 "Cold states:\nMinnesota\nMaine\nMichigan"

这段代码是一个非常好的闭包(closure)示例,在 JavaScript 中。闭包是一种能够捕获和保持外部函数作用域变量的内部函数。在这个特定的例子中,我们定义了一个名为 stringAccumulator 的函数,它创建并返回一个内部函数,这个内部函数能够访问并修改 stringAccumulator 函数作用域内的变量。

让我们逐步分析这段代码:

  1. 定义 stringAccumulator 函数:

    function stringAccumulator(initialValue) {
        let val = initialValue;
        return function(s) {
          val += '\n' + s;
          console.log(val);
        };
    }
    

    这个函数接受一个参数 initialValue 并在其内部声明一个局部变量 val,初始值为 initialValue。然后,它返回一个新的匿名函数,这个匿名函数接受一个参数 s 并执行两个操作:

    • 更新 val 的值,将 s 附加到 val 上,并在它们之间添加一个换行符。
    • 打印更新后的 val 值。
  2. 创建一个闭包实例:

    const addColdStates = stringAccumulator('Cold states:');
    

    通过调用 stringAccumulator 并传递 'Cold states:' 作为初始值,创建了一个闭包 addColdStates。这个闭包记住了它被创建时 val 的值(即 'Cold states:')。

  3. 调用闭包函数: 每次调用 addColdStates 函数时,都会向 val 字符串追加新的内容,并打印出来。重要的是,每次调用都是在同一个 val 实例上操作,因为 addColdStates 是一个闭包,它保留了对其创建时作用域中的 val 变量的引用。因此,每次调用都会在前一次调用的结果基础上进行追加。

所以,当你依次调用 addColdStates('Minnesota')addColdStates('Maine')addColdStates('Michigan') 时,输出将会是:

Cold states:\nMinnesota
Cold states:\nMinnesota\nMaine
Cold states:\nMinnesota\nMaine\nMichigan

在这个例子中,闭包允许 addColdStates 函数在连续调用之间“记住”变量 val 的值,这是因为 addColdStates 函数保持着对 stringAccumulator 函数作用域内变量的访问权限。这是 JavaScript 中闭包的一个经典用途,用于创建可以维护自己的私有状态的函数。


函数作为参数

function compose(f, g) {
    const s = "composing";
    const h = function(x) {
        console.log(x);
        f(g(x)); 
    };
    return h;
}
const frankenstein = compose(console.log, Math.sqrt);
frankenstein(9);

是的,这个代码示例涉及到将函数作为参数。在 JavaScript 中,函数是一等公民(first-class citizens),这意味着它们可以像任何其他值一样被传递和操作。这包括将函数作为参数传递给另一个函数,这是高阶函数(higher-order functions)的一个关键特性。

在您提供的 compose 函数示例中:

  1. 将函数作为参数: compose 函数接收两个函数 fg 作为其参数。这是将函数作为参数传递的直接示例。

  2. 返回一个新函数: compose 函数返回一个新的函数 h。这个新函数 h 在被调用时,会依次调用 gf,将 g 的输出作为 f 的输入。

  3. 函数组合: 当 compose 被调用以创建 frankenstein 函数时,它接收了 console.logMath.sqrt 作为参数。这意味着 frankenstein 函数在内部首先会对其输入调用 Math.sqrt,然后将结果传递给 console.log


原型(prototype)

在 JavaScript 中,prototype 是一个非常核心的概念,涉及到多个重要的知识点。以下是与 prototype 相关的主要知识:

1. 原型对象(Prototype Object)

  • 每个 JavaScript 函数都有一个 prototype 属性,这个属性指向函数的原型对象。
  • 原型对象是一个普通对象,用于共享属性和方法。

2. 原型链(Prototype Chain)

  • JavaScript 中的对象都有一个指向它的原型对象的链接(通常是通过内部的 __proto__ 属性实现的)。
  • 当试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或到达原型链的顶端(通常是 Object.prototype)。

3. 构造函数(Constructor Function)

  • 使用构造函数(通过 new 关键字)创建的每个对象都会自动拥有一个指向该构造函数的 prototype 属性的链接。
  • 这意味着所有由同一个构造函数创建的对象都会共享同一个原型对象。

4. 继承(Inheritance)

  • JavaScript 通过原型链实现继承。如果属性在原型链中被找到,那么这个属性就会被使用,这是一种原型继承的表现

  • 当访问一个对象的属性时,如果这个属性在对象本身上不存在,JavaScript 会查找它的原型链。子对象可以继承父对象原型上的方法和属性。

5. Object.prototype

  • 所有的原型链最终都会指向 Object.prototype,除非显式改变一个对象的原型到另一个对象或 null
  • Object.prototype 提供了很多内建的方法和属性,比如 toString()hasOwnProperty()

6. 原型和构造函数的关系

  • 每个原型对象都有一个指向关联构造函数的 constructor 属性。
  • 反过来,每个构造函数都有一个 prototype 属性,指向它的原型对象。

7. 函数的原型

  • 函数也是对象,它们的原型是 Function.prototype
  • 所有函数都继承自 Function.prototype,它包括 call(), apply(), bind() 等方法。
function MyConstructor() {
    // 构造函数内容
    this.x=1
}
MyConstructor.prototype.myMethod = function(){
  return "Hello";
};
MyConstructor.prototype.hello = function(n){n=>console.log(n)}
let myObject = new MyConstructor();

console.log("prototype === MyConstructor.prototype ----->",Object.getPrototypeOf(myObject) === MyConstructor.prototype); // 输出 true
console.log('      Object.getPrototypeOf(myObject) ----->', Object.getPrototypeOf(myObject))
console.log("              MyConstructor.prototype ----->", MyConstructor.prototype)
console.log(Function.prototype) // {}
console.log(Object.prototype)   // [Object: null prototype] {}
console.log("myObject.prototype: ", myObject.prototype) //特殊的对象属性(构造函数中)
console.log({}.prototype) //和shang面的相等 对象直接继承 其构造函数的 原型对象,而不是构造函数自身的属性和方法

当你使用构造函数创建一个对象时,该对象确实继承了构造函数的 .prototype 属性所指向的原型对象的属性和方法。但这里有一个重要的概念需要明确:对象直接继承自 其构造函数的原型对象,而不是构造函数自身的属性和方法。

让我们分解这个过程来理解它:

构造函数和它的 .prototype 属性

构造函数的 .prototype 属性:这个属性是一个对象,包含了所有通过这个构造函数创建的对象所共享的属性和方法。当你定义一个构造函数时,JavaScript 会自动为这个函数创建一个 .prototype 属性。

创建对象

当你使用 new 关键字和构造函数创建一个新对象时,以下步骤会发生:

  1. 创建新对象:JavaScript 引擎创建一个新的空对象。

  2. 设置原型链:新对象的内部 [[Prototype]] 链接(通常通过 __proto__ 属性访问)被设置为构造函数的 .prototype 属性所指向的对象。这意味着新对象将继承 .prototype 上的所有属性和方法。

  3. 执行构造函数:构造函数内的代码被执行,通常用于初始化新对象的属性。

  4. 返回新对象:除非构造函数显式返回一个不同的对象,否则这个新创建的对象会被返回。

继承

  • 新对象继承自构造函数的 .prototype。这意味着如果你在构造函数的 .prototype 上添加了方法或属性,所有通过这个构造函数创建的对象都会共享这些方法和属性。
  • 但是,构造函数自身的属性和方法不会被新对象继承。只有附加在 .prototype 上的属性和方法才会被继承。

示例

function MyConstructor() {
    this.instanceProperty = 'Instance Property';
}

// 添加到原型
MyConstructor.prototype.prototypeProperty = 'Prototype Property';
MyConstructor.numbs=1
// 创建新对象
const obj = new MyConstructor();
console.log(obj.instanceProperty); // 输出 'Instance Property'
console.log(obj.prototypeProperty); // 输出 'Prototype Property'
console.log(obj.numbs)      // 输出 'undefined'

在这个例子中,obj 对象继承了 MyConstructor.prototype 上的 prototypeProperty,但它不会继承 MyConstructor 函数自身的任何属性(除非这些属性是在构造函数内部赋给 this 的,就像 instanceProperty)。

当你使用构造函数创建一个实例时,该实例的原型链将遵循特定的路径以查找属性和方法。这个原型链的结构是 JavaScript 实现继承的关键机制。让我先描述一下这个过程,然后我将为你提供一个图示来更直观地展示这个过程。

描述原型链

假设你有一个构造函数 MyConstructor,并且你使用它来创建了一个实例 myInstance

function MyConstructor(){
    this.x="hello"
}
myInstance= new MyConstructor()
console.log(MyConstructor.prototype) //{}
console.log(Function.prototype)  //{}
console.log(Object.getPrototypeOf(myobj)) //{}
console.log(myobj) //MyConstuctor { x: 'hello', nw: 1 }
  1. 实例 myInstance

    • 它首先包含所有在 MyConstructor 函数中通过 this 关键字添加的属性和方法。
    • 如果你尝试访问 myInstance 上不存在的属性或方法,JavaScript 将查找它的原型。
  2. 原型 MyConstructor.prototype

    • myInstance 的原型是 MyConstructor.prototype。这意味着 myInstance 可以访问在 MyConstructor.prototype 上定义的所有属性和方法。
    • 每个函数都是 Function 的实例,包括由 function 关键字创建的函数、函数表达式、箭头函数,甚至是 Function 构造函数本身。函数可以访问 Function.prototype 中定义的方法,比如 call, apply, 和 bind
    • Function.prototype 本身是一个对象,这意味着它继承自 Object.prototype。如果在 MyConstructor.prototype 上没有找到该属性或方法,JavaScript 继续沿原型链向上查找。
    • Function.prototype 本身是一个对象,这意味着它继承自 Object.prototype
Function.prototype === Myconstructor.prototype 
//{}
  1. Object.prototype
    • 如果属性或方法不在 MyConstructor.prototype 上,JavaScript 会继续在 Object.prototype 上查找,这是所有对象的原型,并提供了如 toString()hasOwnProperty() 等方法。
    • Object.prototype 是原型链的最顶端。如果在这里也没有找到属性或方法,那么结果将是 undefined
ob={
    2:2,
    x:'s'
}
console.log(ob.toString)  //[Function: toString]'
console.log(ob.toString())  // [object Object]'

hasOwn和hasOwnProperty的区别

const object = { name: "Alice" };
console.log("name-----------|")
console.log(Object.hasOwn(object, "name")); // 输出 true
console.log(object.hasOwnProperty("name")); // 输出 true
// 和下面的使用方式相比较
console.log("toString-----------|")
console.log(object.hasOwnProperty("toString")); // 输出 false 对象实例的方法
console.log(Object.hasOwn(object, "toString")); // 输出 false 静态方法 static
//Object.hasOwn更安全,更现代

问题原因

当直接在对象上调用hasOwnProperty方法时,如果对象恰好有一个自己的属性名叫做hasOwnProperty,那么这会导致调用的不是Object.prototype上的方法,而是对象自身的属性,这可能导致程序运行异常。此外,如果对象是通过Object.create(null)创建的,它将不会继承Object.prototype,因此也不会有hasOwnProperty方法,直接调用会导致错误。

// 不推荐的方式,可能引起 ESLint 警告
if (obj.hasOwnProperty('propName')) {
    //something
}

// 推荐的方式
if (Object.prototype.hasOwnProperty.call(obj, 'propName')) {
    //something
}

这种写法通过call方法改变了hasOwnProperty的调用上下文为obj,这样就算obj上有自己的hasOwnProperty属性或方法,也不会影响到这里的调用,从而保证了代码的安全性和可靠性。


  1. Rest操作符 (...):允许我们将一个不确定数量的参数表示为一个数组。当我们在函数定义中的最后一个参数位置使用它时,它会将余下的参数收集到一个数组中。用于将一个数组或者对象展开成为0个或多个元素(在数组字面量中)或属性(在对象字面量中)。

  2. Call/Apply/Bind:这些都是函数原型上的方法,允许我们显式地设置函数调用时的this值,或者在调用函数时传递参数列表(callapply)或返回一个新函数(bind),其中this值已经被绑定。

  3. 数组方法

    • filter:创建一个新数组,包含通过所提供函数实现的测试的所有元素。
    • map:创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
    • reduce:对数组中的每个元素执行一个由您提供的“reducer”函数(升序执行),将其结果汇总为单个返回值。
  4. call 方法 在 JavaScript 中,function.call() 是一种非常有用的方法,它允许你调用一个函数,同时可以指定该函数内部的 this 值(即上下文)。这个方法还可以接受一个参数列表,作为调用函数时的参数。

这是 Function.prototype.call() 方法的基本语法:

func.call([thisArg[, arg1, arg2, ...]])
  • func:要调用的函数。
  • thisArgfunc 函数运行时使用的 this 值。注意,thisArg 的值并不一定是实际的 this 值,它可能是任何值,甚至是 nullundefined,具体取决于函数的编写方式(严格模式下的行为与非严格模式下不同)。
  • arg1, arg2, ...:传递给函数的参数。

这个方法的一个常见用途是调用拥有类似结构的对象的方法。例如,你可以使用一个对象的方法来操作另一个对象。

下面是一个简单的例子:

function showDetails(age, nationality) {
  console.log(this.name + " is " + age + " years old and is " + nationality);
}

var person1 = {
  name: "John"
};

// 调用 showDetails 函数,将 person1 作为 this 的值
showDetails.call(person1, 25, "American");
// 输出: "John is 25 years old and is American"

在这个例子中,showDetails 函数原本并不是 person1 对象的一部分,但使用 call 方法,我们可以把 person1 作为 this 传入 showDetails 函数,从而“借用”这个函数。这使得 this.nameshowDetails 函数内部引用的是 person1name 属性。同时,我们也直接传递了 agenationality 作为参数。


在 JavaScript 中,mapreducefilter 是三个非常有用的数组方法,它们都会返回一个值,而不仅仅是用作循环机制。这些方法都是以函数式编程风格工作的,可以帮助你写出更简洁、更易读的代码。

每个方法都非常强大,可以用于各种数据处理场景。最重要的是,它们都返回一个新值(或新数组),这意味着原始数组不会被修改,这是函数式编程中的一个核心概念,即不改变状态(不可变性)。使用这些方法可以帮助你编写出更纯净、更易于理解和维护的代码。

Map in JavaScript

在 JavaScript 中,map 方法创建一个新数组,它允许你对数组中的每个元素执行一个函数,并返回一个新数组,这个新数组包含了应用该函数后的结果。

const arr = [1, 2, 3, 4];
const result = arr.map(function(element) {
  return element * 2;
});
// result: [2, 4, 6, 8]

在这个例子中,result 是原数组 arr 中每个元素乘以 2 后的新数组。

const numbers = [NaN, NaN, 100];
const mappedResult = numbers.map(isNaN);
console.log(mappedResult);

这段代码做了以下几步操作:

  1. 定义了一个名为 numbers 的数组,包含三个元素:NaN, NaN, 和 100
  2. 使用 map 方法对 numbers 数组中的每个元素执行 isNaN 函数。isNaN 函数用于检查一个值是否是“非数字”(Not-a-Number)。
  3. map 方法返回一个新的数组,该数组包含了对原数组中每个元素执行 isNaN 后的结果。

在这个例子中,isNaN(NaN) 返回 true,因为 NaN 是非数字。而 isNaN(100) 返回 false,因为 100 是一个有效的数字。因此,mappedResult 数组将是 [true, true, false]

filter

filter 方法创建一个新数组,包含通过所提供函数实现的测试的所有元素。

const arr = [1, 2, 3, 4, 5];
const result = arr.filter(function(element) {
  return element % 2 === 0;
});
// result: [2, 4]

这里,result 包含了 arr 中所有偶数的元素,因为只有它们通过了测试 element % 2 === 0

reduce

  1. reduce() 方法在数组中从左到右依次处理(遍历)数组的每个元素。
  2. 对于每个元素,它执行提供的 reducer 函数,将结果传递给下一次迭代的累加器。
  3. 这个过程一直持续到数组的末尾。
  4. 最终,reduce() 返回单个值,即函数累计处理的结果。

如果不提供初始值,reduce() 会使用数组的第一个元素作为累加器的初始值,并从数组的第二个元素开始执行 reducer 函数。

const numbers = [1, 2, 3, 4];
const sum = numbers.reduce(function(accumulator, currentValue) {
  return accumulator + currentValue;
}, 0); // 初始值为 0

console.log(sum); // 输出: 10

在这个例子中,reduce() 方法将数组 numbers 中的元素相加,并返回它们的和。初始值为 0,这意味着累加器的初始值是 0。对于数组中的每个元素,它都添加到累加器中,最终得到总和并返回。

concat用法

  • concat 方法不会改变现有数组,它会返回一个新数组。
  • 如果参数中包含数组,则concat会将这些数组的元素添加到新数组中。如果参数是非数组值,则这些值会直接添加到新数组的末尾。
  • 可以同时合并多个数组和/或值。 合并三个数组
const array1 = [1, 2];
const array2 = [3, 4];
const array3 = [5, 6];
const combinedArray = array1.concat(array2, array3);

console.log(combinedArray);
// 输出: [1, 2, 3, 4, 5, 6]

合并数组和单独的值

const array = [1, 2, 3];
const newValue = 4;
const newArray = array.concat(newValue, 5);

console.log(newArray);
// 输出: [1, 2, 3, 4, 5]

Slice in JavaScript

这段代码展示了 slice 方法的用法以及对 slice 方法的两种不同检测:

const numbers = [1, 2, 3, 4];
const numbersCopy = numbers.slice();
console.log('slice' in numbers);
console.log(numbers.hasOwnProperty('slice'));
  1. 首先,你创建了一个名为 numbers 的数组,包含数字 1, 2, 3, 4。

  2. 接着,你使用了 slice() 方法来创建这个数组的一个副本,命名为 numbersCopyslice 方法在没有指定参数的情况下会复制整个数组。

  3. 然后,你用两种方式检查 slice 方法是否存在于 numbers 数组中。

    • console.log('slice' in numbers); 这行代码检查 slice 是否是 numbers 对象的一个属性(无论它是自身的还是继承的)。因为 slice 是数组的原型方法,所以它实际上存在于 Array.prototype 中,而不是数组实例本身。因此,这行代码将输出 true

    • console.log(numbers.hasOwnProperty('slice')); 这行代码使用 hasOwnProperty 方法检查 slice 是否是 numbers 对象自身的属性。由于 slice 是从 Array.prototype 继承的,而不是数组对象自身的直接属性,所以这行代码将输出 false

综上所述,'slice' in numbers 会输出 true,因为 slice 是数组的一个方法(通过原型继承),而 numbers.hasOwnProperty('slice') 会输出 false,因为 slice 不是数组对象自身的属性。


Object 继承

const duck = {
    name:'Quincy',
    quack() {
        console.log('Quack!')
    }
};
const duckling = Object.create(duck);
console.log(`${duck.name},${duckling.name}`);

这段代码展示了 JavaScript 中对象的原型继承。让我们逐步分析:

  1. 创建 duck 对象:

    const duck = {
        name: 'Quincy',
        quack() {
            console.log('Quack!');
        }
    };
    

    这里定义了一个名为 duck 的对象,它有一个属性 name 和一个方法 quack

  2. 使用 Object.create 创建 duckling 对象:

    const duckling = Object.create(duck);
    

    这行代码使用 Object.create 方法创建了一个新对象 duckling,其原型被设置为 duck 对象。这意味着 duckling 会继承 duck 的所有属性和方法。但是,请注意,duckling 自己并没有 name 属性;它只是通过原型链从 duck 继承了这个属性。

  3. 打印 duckducklingname 属性:

    console.log(`${duck.name},${duckling.name}`);
    

    这行代码打印了 duckducklingname 属性。由于 duckling 没有自己的 name 属性,它通过原型链访问 duckname 属性。因此,这行代码将输出 'Quincy,Quincy'

总结来说,这段代码演示了如何通过原型继承在 JavaScript 中创建对象。duckling 对象继承了 duck 对象的属性和方法。在这个特定的例子中,duckling 对象没有自己的 name 属性,因此它通过原型链回溯到 duck 对象并使用了 duckname 属性。


theme: github