Skip to content

你需要知道的 javascript 的细节

统计信息:字数 15681 阅读32分钟

原作者,白霸天——阿里巴巴前端工程师

原文链接:https://zhuanlan.zhihu.com/p/35617827

现在的前端框架层出不穷,3个月就要重新入门一次前端的现状,让我们来不及学好基础就开始上手框架。常常就因为这样,我们会很快到达基础基础技术瓶颈,基础是所有技术的核心,在跳槽季重新温故了一遍 javascript 基础,有收获,整理出来分享给大家。

对象

变量可以当对象使用

javascript 中所有的变量都可以当做对象使用,除了undefinednull ,我们测试下

false.toString() // "false"

[1,2,3].toString() //"1,2,3"

1..toString() //"1"

({a:'33'}).toString() //"[object Object]"
undefined.toString() //Uncaught TypeError

null.toString()   //Uncaught TypeError

数值和对象虽然能调用 toString 方法,但是在写法上需要注意下

number 调用时不能直接数值后面直接调用toString 方法,因为 js 会将点运算符解析为数值的小数点

1.toString() //Uncaught SyntaxError

1..toString() //"1"

对象直接调用toString 方法时,需要用小括号包裹起来,不然js 会将对象的花括号识别成块,从而报错

{a:'33'}.toString()  // Uncaught SyntaxError

({a:'33'}).toString() // "[object Object]"

对象删除属性

删除对象的属性唯一的方法是使用 delete 操作符,设置元素属性为 undefined 或则 null 并不能真正删除,只是移除了属性和值的关联

var test = {
    name:'bbt',
    age:'18',
    love:'dog'
}

test.name = undefined
test.age = null
delete test.love

for (var i in test){
  console.log(i+':'+test[i])
}

运行结果

name:undefined
age:null
undefined

只有 love 被正则删除,nameage 还是能被遍历到

构造函数

javascript 中,通过关键字 new 调用的函数就被认为是构造函数,我们可以通过构造函数创建对象实例

但是在使用过程中你一定发现了,每实例化一个对象,都会在实例对象上创造构造函数的方法和属性。倘若创建的实例比较多,重复创建同一个方法去开辟内存空间就会显得十分浪费,我们可以通过把被经常复用的方法放在原型链上。

原型继承

javascript 和一些我们所了解的面向对象编程的语言不太一样,在 es6 语法以前,我们是通过原型链来实现方法和属性的继承

function Child(){
  this.name = 'bbt'
}

Child.prototype = {
    title:'baba',
    method: function() {}
};

function Grandson(){}

//设置 Grandson 的 prototype 为 Child 的实例
Grandson.prototype = new Child()

//为 Grandson 的原型添加添加属性 age
Grandson.prototype.age = 40

// 修正 Grandson.prototype.constructor 为 Grandson 本身
Grandson.prototype.constructor = Grandson;

var xiaomin = new Grandson()

//原型链如下
xiaomin // Grandson的实例
    Grandson.prototype // Child的实例
         Grandson.prototype //{title:'baba',...}
            Object.prototype
                {toString: ... /* etc. */};

对象的属性查找,javascript 会在原型链上向上查找属性,直到查到 原型链顶部,所以,属性在原型链的越上端,查找的时间会越长,查找性能和复用属性方面需要开发者自己衡量下。

获取自身对象属性

hasOwnProperty 方法能够判断一个对象是否包含自定义属性,而不是在原型链上的属性

var test = {hello:'123'}

Object.prototype.name = 'bbt'

test.name  //'bbt'
test.hasOwnProperty('hello') //true
test.hasOwnProperty('name') //false

for in 循环可以遍历对象原型链上的所有属性,如此我们将 hasOwnProperty 结合循环for in 能够获取到对象自定义属性

var test = {hello:'222'}
Object.prototype.name = 'bbt'

for(var i in test){
  console.log(i) // 输出两个属性,hello ,name
}


for(var i in test){
  if(test.hasOwnProperty(i)){
    console.log(i)//只输出 hello
  }
}

除了上面的方法,getOwnPropertyNamesObject.keys 方法,能够返回对象自身的所有属性名,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名。

var test = {hello:'222'}
Object.prototype.name = 'bbt'

Object.keys(test) //["hello"]
Object.getOwnPropertyNames(test) //["hello"]

getOwnPropertyNamesObject.keys 的用法有什么区别呢

Object.keys方法只返回可枚举的属性,Object.getOwnPropertyNames 方法还返回不可枚举的属性名。

var a = ['Hello', 'World'];

Object.keys(a) // ["0", "1"]

Object.getOwnPropertyNames(a) // ["0", "1", "length"]  // length 是不可枚举属性

函数

函数声明的变量提升

我们通常会使用函数声明或函数赋值表达式来定义一个函数,函数声明和变量声明一样都存在提升的情况,函数可以在声明前调用,但是不可以在赋值前调用

函数声明

foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}

函数表达式

foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

变量提升是在代码解析的时候进行的,foo() 方法调用的时候,已经在解析阶段将 foo 定义过了。赋值语句只在代码运行时才进行,所以在赋值前调用会报错

一种比较少用的函数赋值操作,将命名函数赋值给一个变量,此时的函数名只对函数内部可见

var test = function foo(){
  console.log(foo) //正常输出
}

console.log(foo) //Uncaught ReferenceError

this 的工作原理

javascript 中 ,this 是一个比较难理解的点,不同的调用环境会导致 this 的不同指向,但是唯一不变的是 this 总是指向一个对象

简单的说,this 就是属性和方法当前所在的对象(函数执行坐在的作用域),平时使用的 this 的情况可以大致分为5种

调用方式指向1. 全局范围调用指向 window 全局对象2. 函数调用指向 window 全局变量3. 对象的方法调用指向方法调用的对象4. 构造函数调用指向构造函数创建的实例5. 通过,call ,apply ,bind 显示的指定 this指向和传参有关

Function.call

语法:function.call(thisArg, arg1, arg2, …),thisArg表示希望函数被调用的作用域,arg1, arg2, …表示希望被传入函数额参数 , 如果参数为空、nullundefined,则默认传入全局对象。

代码示例

var name = 'xiaomin'
var test = {name : 'bbt'}

function hello( _name ){
  _name ?console.log(this.name,_name): console.log(this.name)
}

hello() //xiaomin
hello.call(test) //bbt
hello.call(test,'xiaohong') //bbt xiaohong
hello.call() //xiaomin
hello.call(null) //xiaomin
hello.call(undefined) //xiaomin

Function.apply

语法和call 方法类似,不同的是,传入调用函数的参数变成以数组的形式传入,即 func.apply(thisArg, [argsArray])

改造上面的示例就是

hello.apply(test,['xiaomin'])

Function.bind

bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

var d = new Date();
d.getTime()

var print = d.getTime; //赋值后 getTime 已经不指向 d 实例
print() // Uncaught TypeError

解决方法

var print = d.getTime.bind(d)

容易出错的地方

容易出错的地方,函数调用,this 总是指向 window 全局变量,所以在对象的方法里如果有函数的调用的话(闭包的情况),this 是会指向 全局对象的,不会指向调用的对象,具体示例如下

var name = 'xiaomin'
var test = {
  name : 'bbt'
}
test.method = function(){
  function hello(){
      console.log(this.name)
    }
    hello()
}

// 调用
test.method() // 输出 xiaomin

如果需要将 this 指向调用的对象,可以将对象的 this 指向存储起来,通常我们使用 that 变量来做这个存储。改进之后的代码

var name = 'xiaomin'
var test = {
  name : 'bbt'
}
test.method = function(){
  var that = this
  function hello(){
      console.log(that.name)
    }
    hello()
}

// 调用
test.method() // 输出 bbt

闭包和引用

闭包我们可以理解成是在函数内部定义的函数

javascript 中,内部作用域可以访问到外部作用域的变量,但是外部作用域不能访问内部作用域,需要访问的时候,我们需要通过创建闭包,来操作内部变量

function test(_count){
  var count = _count

  return {
    inc:function(){
      count++
    },
    get:function(){
      return count
    }
  }
}

var a = test(4)
a.get()//4
a.inc()
a.get()//5

闭包中常会出错的面试题

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 0);
}

很多同学会觉得,上面的代码会正常输出0到9,但是实际是输出十次10。遇到这个题目,除了闭包的概念要理解清楚,你还需要知道,setTimeout 内的代码会被异步执行,代码会先执行所有的同步代码,即上面的这段代码会先将 for 循环执行,此时 i 的值为 10,console.log(i) 一直引用着全局变量的 i 所以会输出十次 10

改进代码,我们在 for 循环里创建一个闭包,把循环自增的 i 作为参数传入

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}

setTimeout && setInterval

javascript 是异步的单线程运行语言,其他代码运行的时候可能会阻塞 setTimeout && setInterval 的运行

console.log(1)
setTimeout(function(){
  console.log(2)
}, 0);
console.log(3)

输出结果 132  //setTimeout 被阻塞

处理阻塞的方法是将setTimeoutsetInterval放在回调函数里执行

function test(){
      setTimeout(function(){
          console.log(2)
    }, 0);
}

setTimeoutsetInterval 被调用时会返回一个 ID 用来清除定时器

手工清除某个定时器

var id = setTimeout(foo, 1000);
clearTimeout(id);

清楚所有的定时器

var lastId = setTimeout(function(){
  console.log('11')
}, 0);

for(var i=0;i<lastId;i++;){
  clearTimeout(i);
}

获取最后一个定时器的id,遍历清除定时器,可以清除所有的定时器。

类型

包装对象

数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。

我们可以通过构造函数,将原始类型转化为对应的对象即包装对象,从而是原始类型能够方便的调用某些方法

数值,字符串,布尔值的类型转换函数分别是 Number,String,Boolean,在调用的时候在函数前面加上New 就变成了构造函数,能够蒋对应的原始类型转化为“包装对象”

var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);

typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"

v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

类型转换

类型转换分为强制类型转换和自动转换,javascript 是动态类型语言,在到吗解析运行时,需要的数据类型和传入的数据类型不一致的时候,javascript 会进行自动类型转化。当然,你也可以通过类型转换方法进行强制类型装换。

日常开发中,我们最常用的数据类型自动转换不过就下面三种情况

不同数据类型之间相互运算

'2'+4 // '24'

对非布尔值进行布尔运算

if('22'){
  console.log('hello')
}

对非数据类型使用一元运算符

+'12' //12

我们也通过 Number ,String,Boolean 来进行强制数据类型转换。强制类型转化的规则有点复杂,我们来了解一下。

Number 转换 引用阮老师的详细解释

第一步调用对象自身的valueOf方法如果返回原始类型的值则直接对该值使用Number函数不再进行后续步骤

第二步如果 valueOf 方法返回的还是对象则改为调用对象自身的 toString 方法如果 toString 方法返回原始类型的值则对该值使用 Number 函数不再进行后续步骤

第三步如果 toString 方法返回的是对象就报错

String 转换方法同样也是通过调用原对象的 toString 方法和 valueOf 方法,但是不同的是 String 函数会先调用 toString方法进行转换

Boolean` 的转换规则会相对简单一些,除了几个特殊的值,都会被转化为 `true
undefined
null
+0或-0
NaN
''(空字符串)

但是要注意

Boolean('false') //true

typeof

typeof 操作符返回数据类型,但是由于 javascript 设计的历史原因,typeof 现已经不能满足我们现在对于类型判断的要求了

ValueClassType"foo"Stringstringnew String("foo")Stringobject1.2Numbernumbernew Number(1.2)NumberobjecttrueBooleanbooleannew Boolean(true)Booleanobjectnew Date()Dateobjectnew Error()Errorobject[1,2,3]Arrayobjectnew Array(1, 2, 3)Arrayobjectnew Function("")Functionfunctio/abc/gRegExpobject (function in Nitro/V8)new RegExp("meow")RegExpobject (function in Nitro/V8){}Objectobjectnew Object()Objectobjectnullnullobject

我们可以看到,typeof 不能区分对象的数组和日期,还会把 null 判断成对象,那我们一般是什么时候用 typeof 呢。我们可以用来判断一个已经定义的变量是否被赋值。

var a
if(typeof a == 'undefined'){
  console.log('a 已经被定义')
}

instanceof

instanceof 操作符通常用来判断,一个对象是否在另一个对象的原型链上,需要注意的是 instanceof 的左值是对象,右值是构造函数

// defining constructors
function C() {}
function D() {}

var o = new C();

// true, because: Object.getPrototypeOf(o) === C.prototype
o instanceof C;

// false, because D.prototype is nowhere in o's prototype chain
o instanceof D;

#### Object.prototype.toString

那么我们有没有可以用来区分变量数据类型的方法呢,有,Object.prototype.toString

一些原始数据类型也有 toString 方法,但是通常他们的 toString 方法都是改造过的,不能进行 数据类型判断,所以我们需要用 Object 原型链上的 toString 方法

var a = 1234
a.toString() // '1234'

Object.prototype.toString.call(a) // "[object Number]"

不同类型返回的结果如下:

1. 数值 [object Number]
 2. 字符串 [object String]
 3.布尔值 [object Boolean]
 4.undefined [object undefined]
 5.null  [object Null]
 6.数组 [object Array]
 7.arguments [object Arguments]
 8.函数 [object function]
 9.Error [object Error]
 10.Date [object Date]
 11.RegExp [object RegExp]
 12.其他对象 [object object]

那么我们就能够通过 Object.prototype.toString 方法,封装一个可以判断变量数据类型的函数了

function type(obj) {
    return Object.prototype.toString.call(obj).slice(8, -1);
}

type(function(){}) //"Function"

这次我们从对象、函数、类型三方面入手了解了javascript 中容易被忽视或则说比较难理解的地方


Last update: November 9, 2024