10个最常见的JavaScript问题

news2024/11/18 0:35:50

如今,JavaScript几乎是所有现代web应用程序的核心。这就是为什么JavaScript问题以及找出导致这些问题的错误是web开发人员的首要任务。

 

用于单页应用程序(SPA)开发、图形和动画以及服务器端JavaScript平台的强大的基于JavaScript的库和框架并不是什么新鲜事。JavaScript在web应用程序开发的世界中确实变得无处不在,因此它是一项越来越重要的技能。

起初,JavaScript可能看起来很简单。事实上,将基本的JavaScript功能构建到网页中对于任何有经验的软件开发人员来说都是一项相当简单的任务,即使他们是JavaScript新手。

然而,这种语言比人们最初认为的要微妙、有力和复杂得多。事实上,JavaScript的许多微妙之处导致了许多常见问题,这些问题阻碍了它的工作——我们在这里讨论了其中的10个问题,在成为一名优秀的JavaScript开发人员的过程中,需要注意和避免这些问题。

问题1:不正确的引用 this

随着JavaScript编码技术和设计模式多年来变得越来越复杂,回调和闭包中的自引用作用域也相应增加,这是造成JavaScript问题的 "this/that 混乱 "的一个相当普遍的来源。

考虑下面代码:

Game.prototype.restart = function () {
    this.clearLocalStorage();
    this.timer = setTimeout(function() {
    this.clearBoard();    // What is "this"?
    }, 0);
};

执行上述代码会出现以下错误:

Uncaught TypeError: undefined is not a function

上述错误的原因是,当调用 setTimeout()时,实际上是在调用 window.setTimeout()。因此,传递给setTimeout()的匿名函数是在window对象的上下文中定义的,它没有clearBoard()方法。

传统的、符合老式浏览器的解决方案是将 this 引用保存在一个变量中,然后可以被闭包继承,如下所示:

Game.prototype.restart = function () {
    this.clearLocalStorage();
    var self = this;   // Save reference to 'this', while it's still this!
    this.timer = setTimeout(function(){
    self.clearBoard();    // Oh OK, I do know who 'self' is!
    }, 0);
};

另外,在较新的浏览器中,可以使用bind()方法来传入适当的引用:

Game.prototype.restart = function () {
    this.clearLocalStorage();
    this.timer = setTimeout(this.reset.bind(this), 0);  // Bind to 'this'
};

Game.prototype.reset = function(){
    this.clearBoard();    // Ahhh, back in the context of the right 'this'!
};

问题2:认为存在块级作用域

JavaScript开发者中常见的混乱来源(也是常见的错误来源)是假设JavaScript为每个代码块创建一个新的作用域。尽管这在许多其他语言中是对的,但在JavaScript中却不是。考虑一下下面的代码:

for (var i = 0; i < 10; i++) {
    /* ... */
}
console.log(i);  // 输出什么?

如果你猜测console.log()的调用会输出 undefined 或者抛出一个错误,那你就猜错了。答案是输出10。为什么呢?

在大多数其他语言中,上面的代码会导致一个错误,因为变量i的 "生命"(即使作用域)会被限制在for块中。

但在JavaScript中,情况并非如此,即使在for循环完成后,变量i仍然在作用域内,在退出循环后仍保留其最后的值。(顺便说一下,这种行为被称为变量提升(variable hoisting)。

JavaScript中对块级作用域的支持是通过let关键字实现的。Let关键字已经被浏览器和Node.js等后端JavaScript引擎广泛支持了多年。

问题3:创建内存泄漏

如果没有有意识地编写代码来避免内存泄漏,那么内存泄漏几乎是不可避免的JavaScript问题。它们的发生方式有很多种,所以我们只重点介绍几种比较常见的情况。

内存泄漏实例1:对不存在的对象的悬空引用

考虑以下代码:

var theThing = null;
var replaceThing = function () {
  var priorThing = theThing; 
  var unused = function () {
     // 'unused'是'priorThing'被引用的唯一地方。
    // 但'unused'从未被调用过
    if (priorThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),  // 创建一个1MB的对象
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);    // 每秒钟调用一次 "replaceThing"。

如果你运行上述代码并监测内存使用情况,你会发现你有一个明显的内存泄漏,每秒泄漏整整一兆字节!而即使是手动垃圾收集器(GC)也无济于事。

因此,看起来我们每次调用 replaceThing 都会泄漏 longStr。但是为什么呢?

每个theThing对象包含它自己的1MB longStr对象。每一秒钟,当我们调用 replaceThing 时,它都会在 priorThing 中保持对先前 theThing 对象的引用。

但是我们仍然认为这不会是一个问题,因为每次通过,先前引用的priorThing将被取消引用(当priorThing通过priorThing = theThing;被重置时)。

而且,只在 replaceThing 的主体和unused的函数中被引用,而事实上,从未被使用。

因此,我们又一次想知道为什么这里会有内存泄漏。

为了理解发生了什么,我们需要更好地理解JavaScript的内部工作。实现闭包的典型方式是,每个函数对象都有一个链接到代表其词法作用域的字典式对象。

如果在replaceThing里面定义的两个函数实际上都使用了priorThing,那么它们都得到了相同的对象就很重要,即使priorThing被反复赋值,所以两个函数都共享相同的词法环境。

但是一旦一个变量被任何闭包使用,它就会在该作用域内所有闭包共享的词法环境中结束。而这个小小的细微差别正是导致这个可怕的内存泄露的原因。

内存泄漏实例2:循环引用

考虑下面代码:

function addClickHandler(element) {
    element.click = function onClick(e) {
        alert("Clicked the " + element.nodeName)
    }
}

这里,onClick有一个闭包,保持对element的引用(通过element.nodeName)。通过将onClick分配给element.click,循环引用被创建;即:element → onClick → element → onClick → element...

有趣的是,即使 element 被从dom中移除,上面的循环自引用也会阻止 element 和onClick被收集,因此会出现内存泄漏。

避免内存泄漏:要点

JavaScript的内存管理(尤其是垃圾回收)主要是基于对象可达性的概念。

以下对象被认为是可达的,被称为 "根":

  • 从当前调用堆栈的任何地方引用的对象(即当前被调用的函数中的所有局部变量和参数,以及闭包作用域内的所有变量)
  • 所有全局变量

只要对象可以通过引用或引用链从任何一个根部访问,它们就会被保留在内存中。

浏览器中有一个垃圾收集器,它可以清理被无法到达的对象所占用的内存;换句话说,当且仅当GC认为对象无法到达时,才会将其从内存中删除。不幸的是,很容易出现不再使用的 "僵尸 "对象,但GC仍然认为它们是 "可达的"。

问题4:双等号的困惑

JavaScript 的一个便利之处在于,它会自动将布尔上下文中引用的任何值强制为布尔值。

但在有些情况下,这可能会让人困惑,因为它很方便。例如,下面的一些情况对许多JavaScript开发者来说是很麻烦的。

// 下面结果都是 'true'
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// 下面也都成立
if ({}) // ...
if ([]) // ...

关于最后两个,尽管是空的(大家可能会觉得他们是 false),{}和[]实际上都是对象,任何对象在JavaScript中都会被强制为布尔值 "true",这与ECMA-262规范一致。

正如这些例子所表明的,类型强制的规则有时非常清楚。因此,除非明确需要类型强制,否则最好使用===和!==(而不是==和!=),以避免强制类型转换的带来非预期的副作用。(== 和 != 会自动进行类型转换,而 === 和 !== 则相反)

另外需要注意的是:将NaN与任何东西(甚至是NaN)进行比较时结果都是 false。

因此,不能使用双等运算符(==, ==, !=, !==)来确定一个值是否是NaN。如果需要,可以使用内置的全局 isNaN()函数。

console.log(NaN == NaN);    // False
console.log(NaN === NaN);   // False
console.log(isNaN(NaN));    // True

问题5:低效的DOM操作

使用 JavaScript 操作DOM(即添加、修改和删除元素)是相对容易,但操作效率却不怎么样。

比如,每次添加一系列DOM元素。添加一个DOM元素是一个昂贵的操作。连续添加多个DOM元素的代码是低效的。

当需要添加多个DOM元素时,一个有效的替代方法是使用 document fragments来代替,从而提高效率和性能。

var div = document.getElementsByTagName("my_div");
    
var fragment = document.createDocumentFragment();

for (var e = 0; e < elems.length; e++) {  // elems previously set to list of elements
    fragment.appendChild(elems[e]);
}
div.appendChild(fragment.cloneNode(true));

除了这种方法固有的效率提高外,创建附加的DOM元素是很昂贵的,而在分离的情况下创建和修改它们,然后再将它们附加上,就会产生更好的性能。

问题6:在循环内错误使用函数定义

考虑下面代码:

var elements = document.getElementsByTagName('input');
var n = elements.length;    // Assume we have 10 elements for this example
for (var i = 0; i < n; i++) {
    elements[i].onclick = function() {
        console.log("This is element #" + i);
    };
}

根据上面的代码,如果有10个 input 元素,点击任何一个都会显示 "This is element #10"。

这是因为,当任何一个元素的onclick被调用时,上面的for循环已经结束,i的值已经是10了(对于所有的元素)。

我们可以像下面这样来解决这个问题:

var elements = document.getElementsByTagName('input');
var n = elements.length;   
var makeHandler = function(num) { 
     return function() {  
         console.log("This is element #" + num);
     };
};
for (var i = 0; i < n; i++) {
    elements[i].onclick = makeHandler(i+1);
}

makeHandler 是一个外部函数,并返回一个内部函数,这样就会形成一个闭包,num 就会调用时传进来的的当时值,这样在点击元素时,就能显示正确的序号。

问题7:未能正确利用原型继承

考虑下面代码:

Baseobject = function(name) {
    if (typeof name !== "undefined") {
        this.name = name;
    } else {
        this.name = 'default'
    }
};

上面代码比较简单,就是提供了一个名字,就使用它,否则返回 default:

var firstObj = new BaseObject();
var secondObj = new BaseObject('unique');

console.log(firstObj.name);  // -> 'default'
console.log(secondObj.name); // -> 'unique'

但是,如果这么做呢:

delete secondObj.name;

会得到:

console.log(secondObj.name); // 'undefined'

当使用 delete 删除该属性时,就会返回一个 undefined,那么如果我们也想返回 default 要怎么做呢?利用原型继承,如下所示:

BaseObject = function (name) {
    if(typeof name !== "undefined") {
        this.name = name;
    }
};

BaseObject.prototype.name = 'default';

IT问答库icon-default.png?t=M85Bhttp://wap.mobiletrain.org/qa/

BaseObject 从它的原型对象中继承了name 属性,值为 default。因此,如果构造函数在没有 name 的情况下被调用,name 将默认为 default。同样,如果 name 属性从BaseObject的一个实例中被移除,那么会找到原型链的 name,,其值仍然是default。所以'

var thirdObj = new BaseObject('unique');
console.log(thirdObj.name);  // -> Results in 'unique'

delete thirdObj.name;
console.log(thirdObj.name);  // -> Results in 'default'

问题8:为实例方法创建错误的引用

考虑下面代码:

var MyObject = function() {}
    
MyObject.prototype.whoAmI = function() {
    console.log(this === window ? "window" : "MyObj");
};

var obj = new MyObject();

现在,为了操作方便,我们创建一个对whoAmI方法的引用,这样通过whoAmI()而不是更长的obj.whoAmI()来调用。

var whoAmI = obj.whoAmI;

为了确保没有问题,我们把 whoAmI 打印出来看一下:

console.log(whoAmI);

输出:

function () {
    console.log(this === window ? "window" : "MyObj");
}

Ok,看起来没啥问题。

接着,看看当我们调用obj.whoAmI() 和 whoAmI() 的区别。

obj.whoAmI();  // Outputs "MyObj" (as expected)
whoAmI();      // Outputs "window" (uh-oh!)

什么地方出错了?当我们进行赋值时 var whoAmI = obj.whoAmI,新的变量whoAmI被定义在全局命名空间。

结果,this的值是 window,而不是 MyObject 的 obj 实例!

因此,如果我们真的需要为一个对象的现有方法创建一个引用,我们需要确保在该对象的名字空间内进行,以保留 this值。一种方法是这样做:

var MyObject = function() {}
    
MyObject.prototype.whoAmI = function() {
    console.log(this === window ? "window" : "MyObj");
};

var obj = new MyObject();
obj.w = obj.whoAmI;   // Still in the obj namespace

obj.whoAmI();  // Outputs "MyObj" (as expected)
obj.w();       // Outputs "MyObj" (as expected)

问题9:为 setTimeout 或 setInterval 提供一个字符串作为第一个参数

首先,需要知道的是为 setTimeout 或 setInterval 提供一个字符串作为第一个参数,这本身并不是一个错误。它是完全合法的JavaScript代码。这里的问题更多的是性能和效率的问题。

很少有人解释的是,如果你把字符串作为setTimeout或setInterval的第一个参数,它将被传递给函数构造器,被转换成一个新函数。这个过程可能很慢,效率也很低,而且很少有必要。

将一个字符串作为这些方法的第一个参数的替代方法是传入一个函数。

setInterval("logTime()", 1000);
setTimeout("logMessage('" + msgValue + "')", 1000);

更好的选择是传入一个函数作为初始参数:

setInterval(logTime, 1000); 
    
setTimeout(function() {      
    logMessage(msgValue);     
}, 1000);

问题10:未使用 "严格模式"

"严格模式"(即在JavaScript源文件的开头包括 "use strict";)是一种自愿在运行时对JavaScript代码执行更严格的解析和错误处理的方式,同时也使它更安全。

但是,不使用严格模式本身并不是一个 "错误",但它的使用越来越受到鼓励,不使用也越来越被认为是不好的形式。

以下是严格模式的一些主要好处:

  • 使得调试更容易。原本会被忽略或无感知的代码错误,现在会产生错误或抛出异常,提醒我们更快地发现代码库中的JavaScript问题,并引导更快地找到其来源。
  • 防止意外的全局变量。在没有严格模式的情况下,给一个未声明的变量赋值会自动创建一个具有该名称的全局变量。这是最常见的JavaScript错误之一。在严格模式下,试图这样做会产生一个错误。
  • 消除this 强迫性。在没有严格模式的情况下,对 null 或 undefined 的 this 值的引用会自动被强制到全局。在严格模式下,引用null或undefined的this值会产生错误。
  • 不允许重复的属性名或参数值。严格模式在检测到一个对象中的重复命名的属性(例如,var object = {foo: "bar", foo: "baz"};)或一个函数的重复命名的参数(例如,function foo(val1, val2, val1){})时抛出一个错误,从而捕捉到你的代码中几乎肯定是一个错误,否则你可能会浪费很多时间去追踪。
  • 使得eval()更加安全。eval()在严格模式和非严格模式下的行为方式有一些不同。最重要的是,在严格模式下,在eval()语句中声明的变量和函数不会在包含的范围内创建。(在非严格模式下,它们是在包含域中创建的,这也可能是JavaScript问题的一个常见来源)。
  • 在无效使用delete的情况下抛出错误。delete 操作符(用于从对象中删除属性)不能用于对象的非可配置属性。当试图删除一个不可配置的属性时,非严格的代码将无声地失败,而严格模式在这种情况下将抛出一个错误。

写在最后

以上就是我今天跟你分享的10个JavaScript中最常见的问题,不知道这10个问题中有没有你不知道?如果有的话,请认真学习,如没有的话,请当作复习。

IT问答库icon-default.png?t=M85Bhttp://wap.mobiletrain.org/qa/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/55181.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

opencv c++ 二值图像、阈值计算方法、全局阈值、自适应阈值

1、图像定义&#xff1a; 彩色图像 &#xff1a;三通道&#xff0c;像素值一般为0~255&#xff1b; 灰度图像&#xff1a;单通道&#xff0c;像素值一般为0~255&#xff1b; 二值图像&#xff1a;单通道&#xff0c;像素值一般为0&#xff08;黑色&#xff09;、255&#xff08…

Python计算器(包含机制转换)

实现思路&#xff1a; 要优先处理内层括号运算&#xff0d;&#xff0d;外层括号运算&#xff0d;&#xff0d;先乘除后加减的原则&#xff1a; 1、正则处理用户输入的字符串&#xff0c;然后对其进行判断&#xff0c;判断计算公式是否有括号&#xff0c;有就先将计算公式进行…

判断二叉树是否是平衡二叉树(c#)

问题描述 给定一棵二叉树&#xff0c;判断其是否为平衡二叉树。 示例 示例1 Input: root [3,9,20,null,null,15,7] Output: true 示例2 Input: root [1,2,2,3,3,null,null,4,4] Output: false 解决方案描述 二叉树的每个节点的左子节点和右子节点的高度差小于等于1&#x…

Windows和Linux混合系统通过AD域实现用户集中认证

一、Windows AD域 1、统一认证简介 管理的Linux服务器和Windows服务器如果很多,如果都用本地用户名管理,要管理和记住几十台甚至上百台服务器的不同账号不同密码,这是很难的。但是如果所有服务器账号密码都设置一样,那又完全没有安全性可言。 什么是服务器的集中认证(统…

数据结构(8)树形结构——B树、B+树(含完整建树过程)

目录 8.1.B树 8.1.1.概述 8.1.2.完整建树过程 8.2.B树 8.1.B树 8.1.1.概述 B树存在的意义&#xff1a; 二叉树在存储数据时可能出现向一边倾斜导致查询效率降低的情况&#xff0c;为了防止二叉树的倾斜&#xff0c;出现了平衡二叉树&#xff0c;通过旋转的方式保证二叉树…

[附源码]计算机毕业设计springboot校园商铺

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

[附源码]Python计算机毕业设计Django基于web的羽毛球管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

学生HTML个人网页作业作品 HTML+CSS+JavaScript环保页面设计与实现制作

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

【PPT计时器】如何在wps演示PPT中使用定时器、计时器功能?不使用第三方插件,仅仅使用第三方计时器软件

一、问题背景和解决思路 很多人在展示PPT时&#xff0c;有精确的时间要求&#xff0c;比如五分钟&#xff0c;十分钟。 这时候&#xff0c;我们希望在演示的时候&#xff0c;PPT上附带一个小计时器、或者定时器。 网上有很多加定时器动画的教程&#xff0c;大多数停留在动画…

【D3.js】1.15-反转 SVG 元素

title: 【D3.js】1.15-反转 SVG 元素 date: 2022-12-02 14:07 tags: [JavaScript,CSS,HTML,D3.js,SVG] 文章目录一、学习目标二、题目三、通关代码参考更新svg坐标的y轴是在顶部的&#xff0c;即画出来的rect也是底朝上&#xff0c;如何让rect的底处于底部呢&#xff1f;一、学…

【C++初阶】STL-string的使用

文章目录一.string初识1.STL简介a.STL的组成b.STL和string的关系2.basic_string二.构造函数三.三种遍历方式四.容量相关的函数1.size()2.reserve()–调整容量3.resize()–调整size五.字符串的增删查改1.assign2.replace3.find()4.substr()5.insert()6.相关应用a.替换空格:b.取出…

【Redis-08】面试题之Redis数据结构与对象-RedisObject(上篇)

Redis本质上是一个数据结构服务器&#xff0c;使用C语言编写&#xff0c;是基于内存的一种数据结构存储系统&#xff0c;它可以用作数据库、缓存或者消息中间件。 我们经常使用的redis的数据结构有5种&#xff0c;分别是&#xff1a;string(字符串)、list(列表)、hash(哈希)、s…

string类的模拟实现

目录 一、浅拷贝、深拷贝 二、传统版本写法的String类 三、现代版本写法的String类 四、String类的模拟实现 一、浅拷贝、深拷贝 构造 //构造函数String(const char* str ""){if (nullptr str){assert(false);return;}_str new char[strlen(str) 1];strcpy(_s…

使用 Pandas 和 SQL 进行实用数据分析,让我们用 pandas 和 SQL 进行数据分析并实际理解它们(教程含数据csv)

Pandas是一种快速、强大、灵活且易于使用的开源数据分析和操作工具, 构建于 Python 编程语言之上。 SQL代表结构化查询语言。SQL 允许您从 RDBMS(关系数据库管理系统)访问数据,并可用于数据分析。 Pandas 和 SQL 都广泛用于数据分析。 在这篇博客中,我们将使用pandas和…

做好自己安全第一责任人 嘀嗒全面上线安全带智能语音提醒

2022年12月2日是第十一个“全国交通安全日”&#xff0c;今年主题为“文明守法 平安回家”。 当天&#xff0c;嘀嗒出行启动主题为“共建三方安全观&#xff0c;安全要靠你我他”共塑行动&#xff0c;倡导平台、用户、行业各方形成合力&#xff0c;共塑共创安全文明的新出行之路…

简单的PCI总线INTx中断实现流程

一个简单的PCI总线INTx中断实现流程,如下图所示。 1. 首先,PCI设备通过INTx边带信号产生中断请求,经过中断控制器(Interrupt Controller,PIC)后,转换为INTR信号,并直接发送至CPU; 2. CPU收到INTR信号置位后,意识到了中断请求的发生,但是此时并不知道是什么中断请求…

记一次 .NET 某电子厂OA系统 非托管内存泄露分析

一&#xff1a;背景 1.讲故事 这周有个朋友找到我&#xff0c;说他的程序出现了内存缓慢增长&#xff0c;没有回头的趋势&#xff0c;让我帮忙看下到底怎么回事&#xff0c;据朋友说这个问题已经困扰他快一周了&#xff0c;还是没能找到最终的问题&#xff0c;看样子这个问题…

hyper-v 虚拟机与本机之间 sftp实现文件传输

hyper-v 主打安全性&#xff0c;所以跟VMware不一样&#xff0c;不能实现复制粘贴&#xff1a;文字、文件、文件夹&#xff0c;所以采取了折中的办法&#xff0c;在Windows主机端用power shell&#xff0c;sftp命令进行文件传输。 前提 需要安装并能够正常运行ssh&#xff0c;后…

C++实现彩色bmp图片转灰度图

简介 BMP&#xff08;全称Bitmap&#xff09;是Windows操作系统中的标准图像文件格式&#xff0c;可以分成两类&#xff1a;设备相关位图&#xff08;DDB&#xff09;和设备无关位图&#xff08;DIB&#xff09;&#xff0c;使用非常广。它采用位映射存储格式&#xff0c;除了…

第05章_存储引擎

第05章_存储引擎1. 查看存储引擎2. 设置系统默认的存储引擎3. 设置表的存储引擎3.1 创建表时指定存储引擎3.2 修改表的存储引擎4. 引擎介绍4.1 InnoDB 引擎:具备外键支持功能的事务存储引擎4.2 MyISAM 引擎:主要的非事务处理存储引擎4.3 Archive 引擎:用于数据存档4.4 Blackhol…