JavaScript常用,继承,原生JavaScript实现classList

原文链接:http://caibaojian.com/8-javascript-attention.html

基于 Class 的组件最佳实践(Class Based Components)

基于 Class 的组件是状态化的,包含有自身方法、生命周期函数、组件内状态等。最佳实践包括但不限于以下一些内容:

1)引入 CSS 依赖 (Importing CSS)

我很喜欢 CSS in JavaScript 这一理念。在 React 中,我们可以为每一个 React 组件引入相应的 CSS 文件,这一“梦想”成为了现实。在下面的代码示例,我把 CSS 文件的引入与其他依赖隔行分开,以示区别:

import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

当然,这并不是真正意义上的 CSS in JS,具体实现其实社区上有很多方案。我的 Github 上 fork 了一份各种 CSS in JS 方案的多维度对比,感兴趣的读者可以点击这里

2)设定初始状态(Initializing State)

在编写组件过程中,一定要注意初始状态的设定。利用 ES6 模块化的知识,我们确保该组件暴露都是 “export default” 形式,方便其他模块(组件)的调用和团队协作。

import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
    state = { expanded: false }
    ......

3)设定 propTypes 和 defaultProps

propTypes 和 defaultProps 都是组件的静态属性。在组件的代码中,这两个属性的设定位置越高越好。因为这样方便其他阅读代码者或者开发者自己 review,一眼就能看到这些信息。这些信息就如同组件文档一样,对于理解或熟悉当前组件非常重要。

同样,原则上,你编写的组件都需要有 propTypes 属性。如同以下代码:

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

Functional Components 是指没有状态、没有方法,纯组件。我们应该最大限度地编写和使用这一类组件。这类组件作为函数,其参数就是 props, 我们可以合理设定初始状态和赋值。

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
    const formStyle = expanded ? {height: 'auto'} : {height: 0}
    return (
        <form style={formStyle} onSubmit={onSubmit}>
            {children}
        <button onClick={onExpand}>Expand</button>
        </form>
    )
}

4)组件方法(Methods)

在编写组件方法时,尤其是你将一个方法作为 props 传递给子组件时,需要确保 this 的正确指向。我们通常使用 bind 或者 ES6 箭头函数来达到此目的。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState({ expanded: !this.state.expanded })
    }

当然,这并不是唯一做法。实现方式多种多样,我专门有一片文章来对比 React 中对于组件 this 的绑定,可以点击此处参考。

5)setState 接受一个函数作为参数(Passing setState a Function)

在上面的代码示例中,我们使用了:

this.setState({ expanded: !this.state.expanded })

这里,关于 setState hook 函数,其实有一个非常“有意思”的问题。React 在设计时,为了性能上的优化,采用了 Batch 思想,会收集“一波” state 的变化,统一进行处理。就像浏览器绘制文档的实现一样。所以 setState 之后,state 也许不会马上就发生变化,这是一个异步的过程。

这说明,我们要谨慎地在 setState 中使用当前的 state,因为当前的state 也许并不可靠。
为了规避这个问题,我们可以这样做:

this.setState(prevState => ({ expanded: !prevState.expanded }))

我们给 setState 方法传递一个函数,函数参数为上一刻 state,便保证setState 能够立刻执行。

关于 React setState 的设计, Eric Elliott 也曾经这么喷过:setState() Gate,并由此展开了多方“撕逼”。作为围观群众,我们在吃瓜的同时,一定会在大神论道当中收获很多思想,建议阅读。

如果你对 setState 方法的异步性还有困惑,可以同我讨论,这里不再展开。

6)合理利用解构(Destructuring Props)

这个其实没有太多可说的,仔细观察代码吧:我们使用了解构赋值。除此之外,如果一个组件有很多的 props 的话,每个 props 应该都另起一行,这样书写上和阅读性上都有更好的体验。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState(prevState => ({ expanded: !prevState.expanded }))
    }

    render() {
        const {model, title} = this.props

        return ( 
            <ExpandableForm 
            onSubmit={this.handleSubmit} 
            expanded={this.state.expanded} 
            onExpand={this.handleExpand}>
                <div>
                    <h1>{title}</h1>
                    <input
                    type="text"
                    value={model.name}
                    onChange={this.handleNameChange}
                    placeholder="Your Name"/>
                </div>
            </ExpandableForm>
        )
    }
}

7)使用修饰器(Decorators)

这一条是对使用 mobx 的开发者来说的。如果你不懂 mobx,可以大体扫一眼。
我们强调使用 ES next decorate 来修饰我们的组件,如同:

@observer
export default class ProfileContainer extends Component {

使用修饰器更加灵活且可读性更高。即便你不使用修饰器,也需要如此暴露你的组件:

class ProfileContainer extends Component {
    // Component code
}
export default observer(ProfileContainer)

8)闭包(Closures)

一定要尽量避免以下用法:

<input
    type="text"
    value={model.name}
    // onChange={(e) => { model.name = e.target.value }}
    // ^ Not this. Use the below:
    onChange={this.handleChange}
    placeholder="Your Name"/>

不要:

onChange = {(e) => { model.name = e.target.value }}

而是:

onChange = {this.handleChange}

原因其实很简单,每次父组件 render 的时候,都会新建一个新的函数并传递给 input。
如果 input 是一个 React 组件,这会粗暴地直接导致这个组件的 re-render,需要知道,Reconciliation 可是 React 成本最高的部分。

另外,我们推荐的方法,会使得阅读、调试和更改更加方便。

9)JSX中的条件判别(Conditionals in JSX)

真正写过 React 项目的同学一定会明白,JSX 中可能会存在大量的条件判别,以达到根据不同的情况渲染不同组件形态的效果。
就像下图这样:

返例

这样的结果是不理想的。我们丢失了代码的可读性,也使得代码组织显得混乱异常。多层次的嵌套也是应该避免的。

针对于此,有很对类库来解决 JSX-Control Statements 此类问题,但是与其引入第三方类库的依赖,还不如我们先自己尝试探索解决问题。

此时,是不是有点怀念if...else?
我们可以使用大括号内包含立即执行函数IIFE,来达到使用 if...else 的目的:

解决思路

当然,大量使用立即执行函数会造成性能上的损失。所以,考虑代码可读性上的权衡,还是有必要好好斟酌的。
我更加建议的做法是分解此组件,因为这个组件的逻辑已经过于复杂而臃肿了。如何分解?请看我这篇文章。

总结

其实所谓 React “最佳实践”,想必每个团队都有自己的一套“心得”,哪里有一个统一套? 本文指出的几种方法未必对任何读者都适用。针对不同的代码风格,开发习惯,拥有自己团队一套“最佳实践”是很有必要的。从另一方面,也说明了 React 技术栈本身的灵活于强大。

这里我们针对JavaScript初学者给出一些技巧和列出一些陷阱。如果你已经是一个砖家,也可以读一读。

1. 你是否尝试过对数组元素进行排序?

JavaScript默认使用字典序(alphanumeric)来排序。因此,[1,2,5,10].sort()的结果是[1, 10, 2, 5]

如果你想正确的排序,应该这样做:[1,2,5,10].sort((a, b) => a - b)

2. new Date() 十分好用

new Date()的使用方法有:

  • 不接收任何参数:返回当前时间;
  • 接收一个参数x: 返回1970年1月1日 + x毫秒的值。
  • new Date(1, 1, 1)返回1901年2月1号。
  • 然而….,new Date(2016, 1, 1)不会在1900年的基础上加2016,而只是表示2016年。

3. 替换函数没有真的替换?

let s = "bob"
const replaced = s.replace('b', 'l')
replaced === "lob" // 只会替换掉第一个b
s === "bob" // 并且s的值不会变

如果你想把所有的b都替换掉,要使用正则:

"bob".replace(/b/g, 'l') === 'lol'

4. 谨慎对待比较运算

// 这些可以
'abc' === 'abc' // true
1 === 1 // true
// 然而这些不行
[1,2,3] === [1,2,3] // false
{a: 1} === {a: 1} // false
{} === {} // false

因为[1,2,3]和[1,2,3]是两个不同的数组,只是它们的元素碰巧相同。因此,不能简单的通过===来判断。·

5. 数组不是基础类型

typeof {} === 'object' // true
typeof 'a' === 'string' // true
typeof 1 === number // true
// 但是....
typeof [] === 'object' // true

如果要判断一个变量var是否是数组,你需要使用Array.isArray(var)

6. 闭包

这是一个经典的JavaScript面试题:

const Greeters = []
for (var i = 0 ; i < 10 ; i++) {
	Greeters.push(function () { return console.log(i) })
}
Greeters[0]() // 10
Greeters[1]() // 10
Greeters[2]() // 10

虽然期望输出0,1,2,…,然而实际上却不会。知道如何Debug嘛?
有两种方法:

  • 使用let而不是var。备注:可以参考Fundebug的另一篇博客 ES6之”let”能替代”var”吗?
  • 使用bind函数。备注:可以参考Fundebug的另一篇博客 JavaScript初学者必看“this”
    Greeters.push(console.log.bind(null, i))

    当然,还有很多解法。这两种是我最喜欢的!

7. 关于bind

下面这段代码会输出什么结果?

//code from http://caibaojian.com/8-javascript-attention.html
class Foo {
    constructor(name) {
        this.name = name
    }
    greet() {
        console.log('hello, this is ', this.name)
    }
    someThingAsync() {
        return Promise.resolve()
    }
    asyncGreet() {
        this.someThingAsync().then(this.greet)
    }
}
new Foo('dog').asyncGreet()

如果你说程序会崩溃,并且报错:Cannot read property ‘name’ of undefined。

1、因为第16行的geet没有在正确的环境下执行。当然,也有很多方法解决这个BUG!

我喜欢使用bind函数来解决问题:

asyncGreet () {
this.someThingAsync()
.then(this.greet.bind(this))
}

这样会确保greet会被Foo的实例调用,而不是局部的函数的this

2、如果你想要greet永远不会绑定到错误的作用域,你可以在构造函数里面使用bind来绑 。

class Foo {
    constructor(name) {
        this.name = name this.greet = this.greet.bind(this)
    }
}

3、你也可以使用箭头函数(=>)来防止作用域被修改。备注:可以参考Fundebug的另一篇博客 JavaScript初学者必看“箭头函数”。

asyncGreet() {
    this.someThingAsync().then(() = >{
        this.greet()
    })
}

8. Math.min()比Math.max()大

Math.min() < Math.max() // false

因为Math.min() 返回 Infinity, 而 Math.max()返回 -Infinity。

这里我们针对JavaScript初学者给出一些技巧和列出一些陷阱。如果你已经是一个砖家,也可以读一读。

1. 你是否尝试过对数组元素进行排序?

JavaScript默认使用字典序(alphanumeric)来排序。因此,[1,2,5,10].sort()的结果是[1, 10, 2, 5]

如果你想正确的排序,应该这样做:[1,2,5,10].sort((a, b) => a - b)

2. new Date() 十分好用

new Date()的使用方法有:

  • 不接收任何参数:返回当前时间;
  • 接收一个参数x: 返回1970年1月1日 + x毫秒的值。
  • new Date(1, 1, 1)返回1901年2月1号。
  • 然而....,new Date(2016, 1, 1)不会在1900年的基础上加2016,而只是表示2016年。

3. 替换函数没有真的替换?

  let s = "bob"
  const replaced = s.replace('b', 'l')
  replaced === "lob" // 只会替换掉第一个b
  s === "bob" // 并且s的值不会变

如果你想把所有的b都替换掉,要使用正则:

  "bob".replace(/b/g, 'l') === 'lol'

4. 谨慎对待比较运算

  // 这些可以
  'abc' === 'abc' // true
  1 === 1         // true
  // 然而这些不行
  [1,2,3] === [1,2,3] // false
  {a: 1} === {a: 1}   // false
  {} === {}           // false

因为[1,2,3]和[1,2,3]是两个不同的数组,只是它们的元素碰巧相同。因此,不能简单的通过===来判断。

5. 数组不是基础类型

  typeof {} === 'object'  // true
  typeof 'a' === 'string' // true
  typeof 1 === number     // true
  // 但是....
  typeof [] === 'object'  // true

如果要判断一个变量var是否是数组,你需要使用Array.isArray(var)

6. 闭包

这是一个经典的JavaScript面试题:

  const Greeters = []
  for (var i = 0 ; i < 10 ; i++) {
    Greeters.push(function () { return console.log(i) })
  }
  Greeters[0]() // 10
  Greeters[1]() // 10
  Greeters[2]() // 10

虽然期望输出0,1,2,...,然而实际上却不会。知道如何Debug嘛?
有两种方法:

7. 关于bind

下面这段代码会输出什么结果?

  class Foo {
    constructor (name) {
      this.name = name
    }
    greet () {
      console.log('hello, this is ', this.name)
    }
    someThingAsync () {
      return Promise.resolve()
    }
    asyncGreet () {
      this.someThingAsync()
      .then(this.greet)
    }
  }
  new Foo('dog').asyncGreet()

如果你说程序会崩溃,并且报错:Cannot read property 'name' of undefined。
因为第16行的geet没有在正确的环境下执行。当然,也有很多方法解决这个BUG!

  • 我喜欢使用bind函数来解决问题:
    asyncGreet () {
      this.someThingAsync()
      .then(this.greet.bind(this))
    }
    这样会确保greet会被Foo的实例调用,而不是局部的函数的this
  • 如果你想要greet永远不会绑定到错误的作用域,你可以在构造函数里面使用bind来绑 。
    class Foo {
      constructor (name) {
        this.name = name
        this.greet = this.greet.bind(this)
      }
    }
  • 你也可以使用箭头函数(=>)来防止作用域被修改。备注:可以参考Fundebug的另一篇博客 JavaScript初学者必看“箭头函数”
    asyncGreet () {
      this.someThingAsync()
      .then(() => {
        this.greet()
      })
    }

8. Math.min()比Math.max()大

  Math.min() < Math.max() // false

因为Math.min() 返回 Infinity, 而 Math.max()返回 -Infinity。

JavaScript六种继承方式详解

继承是面向对象编程中又一非常重要的概念,JavaScript支持实现继承,不支持接口继承,实现继承主要依靠原型链来实现的

原型链

首先得要明白什么是原型链,在一篇文章看懂proto和prototype的关系及区别中讲得非常详细

原型链继承基本思想就是让一个原型对象指向另一个类型的实例

function SuperType() {
    this.property = true
}

SuperType.prototype.getSuperValue = function() {
    return this.property
}

function SubType() {
    this.subproperty = false
}

SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function() {
    return this.subproperty
}

var instance = new SubType() console.log(instance.getSuperValue()) // true

代码定义了两个类型SuperType和SubType,每个类型分别有一个属性和一个方法,SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的

实现的本质是重写原型对象,代之以一个新类型的实例,那么存在SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了

我们知道,在创建一个实例的时候,实例对象中会有一个内部指针指向创建它的原型,进行关联起来,在这里代码SubType.prototype = new SuperType(),也会在SubType.prototype创建一个内部指针,将SubType.prototype与SuperType关联起来

所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,继而在instance在调用getSuperValue()方法的时候,会顺着这条链一直往上找

添加方法

在给SubType原型添加方法的时候,如果,父类上也有同样的名字,SubType将会覆盖这个方法,达到重新的目的。 但是这个方法依然存在于父类中

记住不能以字面量的形式添加,因为,上面说过通过实例继承本质上就是重写,再使用字面量形式,又是一次重写了,但这次重写没有跟父类有任何关联,所以就会导致原型链截断·

function SuperType() {
    this.property = true
}

SuperType.prototype.getSuperValue = function() {
    return this.property
}

function SubType() {
    this.subproperty = false
}

SubType.prototype = new SuperType()

SubType.prototype = {
    getSubValue: function() {
        return this.subproperty
    }
}

var instance = new SubType() console.log(instance.getSuperValue()) // error

问题

单纯的使用原型链继承,主要问题来自包含引用类型值的原型。

function SuperType() {
    this.colors = ['red', 'blue', 'green']
}

function SubType() {}

SubType.prototype = new SuperType()

var instance1 = new SubType() var instance2 = new SubType()

instance1.colors.push('black') console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green", "black"]

在SuperType构造函数定义了一个colors属性,当SubType通过原型链继承后,这个属性就会出现SubType.prototype中,就跟专门创建了SubType.prototype.colors一样,所以会导致SubType的所有实例都会共享这个属性,所以instance1修改colors这个引用类型值,也会反映到instance2中

借用构造函数

此方法为了解决原型中包含引用类型值所带来的问题

这种方法的思想就是在子类构造函数的内部调用父类构造函数,可以借助apply()和call()方法来改变对象的执行上下文

function SuperType() {
    this.colors = ['red', 'blue', 'green']
}

function SubType() {
    // 继承SuperType
    SuperType.call(this)
}

var instance1 = new SubType() var instance2 = new SubType()

instance1.colors.push('black') console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]

在新建SubType实例是调用了SuperType构造函数,这样以来,就会在新SubType对象上执行SuperType函数中定义的所有对象初始化代码

结果,SubType的每个实例就会具有自己的colors属性的副本了

传递参数

借助构造函数还有一个优势就是可以传递参数

function SuperType(name) {
    this.name = name
}

function SubType() {
    // 继承SuperType
    SuperType.call(this, 'Jiang')

    this.job = 'student'
}

var instance = new SubType() console.log(instance.name) // Jiang
console.log(instance.job) // student

问题

如果仅仅借助构造函数,方法都在构造函数中定义,因此函数无法达到复用

组合继承(原型链+构造函数)

组合继承是将原型链继承和构造函数结合起来,从而发挥二者之长的一种模式

思路就是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承

这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性

function SuperType(name) {
    this.name = name this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function() {
    console.log(this.name)
}

function SubType(name, job) {
    // 继承属性
    SuperType.call(this, name)

    this.job = job
}

// 继承方法
SubType.prototype = new SuperType() SubType.prototype.constructor = SuperType SubType.prototype.sayJob = function() {
    console.log(this.job)
}

var instance1 = new SubType('Jiang', 'student') instance1.colors.push('black') console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() // 'Jiang'
instance1.sayJob() // 'student'
var instance2 = new SubType('J', 'doctor') console.log(instance2.colors) // //["red", "blue", "green"]
instance2.sayName() // 'J'
instance2.sayJob() // 'doctor'

这种模式避免了原型链和构造函数继承的缺陷,融合了他们的优点,是最常用的一种继承模式

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

function object(o) {
function F() {}
F.prototype = o
return new F()
}

在object函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例

本质上来说,object对传入其中的对象执行了一次浅复制

var person = {
    name: 'Jiang',
    friends: ['Shelby', 'Court']
}

var anotherPerson = object(person) console.log(anotherPerson.friends) // ['Shelby', 'Court']

这种模式要去你必须有一个对象作为另一个对象的基础

在这个例子中,person作为另一个对象的基础,把person传入object中,该函数就会返回一个新的对象

这个新对象将person作为原型,所以它的原型中就包含一个基本类型和一个引用类型

所以意味着如果还有另外一个对象关联了person,anotherPerson修改数组friends的时候,也会体现在这个对象中

Object.create()方法

ES5通过Object.create()方法规范了原型式继承,可以接受两个参数,一个是用作新对象原型的对象和一个可选的为新对象定义额外属性的对象,行为相同,基本用法和上面的object一样,除了object不能接受第二个参数以外

var person = {
    name: 'Jiang',
    friends: ['Shelby', 'Court']
}
var anotherPerson = Object.create(person) console.log(anotherPerson.friends)

寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数

function createAnother(o) {
    var clone = Object.create(o) // 创建一个新对象
    clone.sayHi = function() { // 添加方法
        console.log('hi')
    }
    return clone // 返回这个对象
}

var person = {
    name: 'Jiang'
}

var anotherPeson = createAnother(person) anotherPeson.sayHi()

基于person返回了一个新对象anotherPeson,新对象不仅拥有了person的属性和方法,还有自己的sayHi方法

在主要考虑对象而不是自定义类型和构造函数的情况下,这是一个有用的模式

寄生组合式继承

在前面说的组合模式(原型链+构造函数)中,继承的时候需要调用两次父类构造函数

父类

function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}

第一次在子类构造函数中

function SubType(name, job) {
// 继承属性
SuperType.call(this, name)
 
this.job = job
}

第二次将子类的原型指向父类的实例

// 继承方法
SubType.prototype = new SuperType()

当使用var instance = new SubType()的时候,会产生两组name和color属性,一组在SubType实例上,一组在SubType原型上,只不过实例上的屏蔽了原型上的

使用寄生式组合模式,可以规避这个问题

这种模式通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

基本思路:不必为了指定子类型的原型而调用父类的构造函数,我们需要的无非就是父类原型的一个副本

本质上就是使用寄生式继承来继承父类的原型,在将结果指定给子类型的原型

function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

该函数实现了寄生组合继承的最简单形式

这个函数接受两个参数,一个子类,一个父类

第一步创建父类原型的副本,第二步将创建的副本添加constructor属性,第三部将子类的原型指向这个副本

function SuperType(name) {
    this.name = name this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function() {
    console.log(this.name)
}

function SubType(name, job) {
    // 继承属性
    SuperType.call(this, name)

    this.job = job
}

// 继承
inheritPrototype(SubType, SuperType)

var instance = new SubType('Jiang', 'student') instance.sayName()

> 补充:直接使用Object.create来实现,其实就是将上面封装的函数拆开,这样演示可以更容易理解

function SuperType(name) {
    this.name = name this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function() {
    console.log(this.name)
}

function SubType(name, job) {
    // 继承属性
    SuperType.call(this, name)

    this.job = job
}

// 继承
SubType.prototype = Object.create(SuperType.prototype)

// 修复constructor
SubType.prototype.constructor = SubType

var instance = new SubType('Jiang', 'student') instance.sayName()

ES6新增了一个方法,Object.setPrototypeOf,可以直接创建关联,而且不用手动添加constructor属性

//code from http://caibaojian.com/6-javascript-prototype.html
// 继承
Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
 
console.log(SubType.prototype.constructor === SubType) // true

使用jQuery可以给元素很方便的添加class和删除class等操作,现在原生的JavaScript也可以实现这个方法了。使用classList可以方便的添加class、删除class、查询class等。

语法:

let elementClass = element.classList;

elementClasses 是一个 DOMTokenList 表示 element 的类属性 。如果类属性未设置或为空,那么 elementClasses.length 返回 0。element.classList 本身是只读的,虽然你可以使用 add() 和 remove() 方法修改它。

方法:

add( String [, String] )

添加指定的类值。如果这些类已经存在于元素的属性中,那么它们将被忽略。

remove( String [,String] )

删除指定的类值。

item ( Number )

按集合中的索引返回类值。

toggle ( String [, force] )

当只有一个参数时:切换 class value; 即如果类存在,则删除它并返回false,如果不存在,则添加它并返回true。
当存在第二个参数时:如果第二个参数的计算结果为true,则添加指定的类值,如果计算结果为false,则删除它

contains( String )

检查元素的类属性中是否存在指定的类值。

示例:

// div是具有class =“foo bar”的<div>元素的对象引用
div.classList.remove("foo");
div.classList.add("anotherclass");

// 如果visible被设置则删除它,否则添加它
div.classList.toggle("visible");

// 添加/删除 visible,取决于测试条件,i小于10
div.classList.toggle("visible", i < 10);

alert(div.classList.contains("foo"));

//添加或删除多个类
div.classList.add("foo","bar");
div.classList.remove("foo", "bar");

兼容性

不兼容Android2.3和iOS4.2的,在移动端上想使用也是有点头疼啊。IE系列的更别说IE9和IE8了。所以目前来看,还是无法在实际中放心的使用,只能用于某些特定的项目等。不过我们可以通过一些shim来实现,或者最下方给出的原生javascript实现。

跨浏览器javascript shim

https://github.com/eligrey/classList.js/blob/master/classList.js

if ("document" in self) {

// Full polyfill for browsers with no classList support
if (!("classList" in document.createElement("_"))) {

(function (view) {

"use strict";

if (!('Element' in view)) return;

var
    classListProp = "classList"
  , protoProp = "prototype"
  , elemCtrProto = view.Element[protoProp]
  , objCtr = Object
  , strTrim = String[protoProp].trim || function () {
    return this.replace(/^s+|s+$/g, "");
  }
  , arrIndexOf = Array[protoProp].indexOf || function (item) {
    var
        i = 0
      , len = this.length
    ;
    for (; i < len; i++) {
      if (i in this && this[i] === item) {
        return i;
      }
    }
    return -1;
  }
  // Vendors: please allow content code to instantiate DOMExceptions
  , DOMEx = function (type, message) {
    this.name = type;
    this.code = DOMException[type];
    this.message = message;
  }
  , checkTokenAndGetIndex = function (classList, token) {
    if (token === "") {
      throw new DOMEx(
          "SYNTAX_ERR"
        , "An invalid or illegal string was specified"
      );
    }
    if (/s/.test(token)) {
      throw new DOMEx(
          "INVALID_CHARACTER_ERR"
        , "String contains an invalid character"
      );
    }
    return arrIndexOf.call(classList, token);
  }
  , ClassList = function (elem) {
    var
        trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
      , classes = trimmedClasses ? trimmedClasses.split(/s+/) : []
      , i = 0
      , len = classes.length
    ;
    for (; i < len; i++) {
      this.push(classes[i]);
    }
    this._updateClassName = function () {
      elem.setAttribute("class", this.toString());
    };
  }
  , classListProto = ClassList[protoProp] = []
  , classListGetter = function () {
    return new ClassList(this);
  }
;
// Most DOMException implementations don't allow calling DOMException's toString()
// on non-DOMExceptions. Error's toString() is sufficient here.
DOMEx[protoProp] = Error[protoProp];
classListProto.item = function (i) {
  return this[i] || null;
};
classListProto.contains = function (token) {
  token += "";
  return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function () {
  var
      tokens = arguments
    , i = 0
    , l = tokens.length
    , token
    , updated = false
  ;
  do {
    token = tokens[i] + "";
    if (checkTokenAndGetIndex(this, token) === -1) {
      this.push(token);
      updated = true;
    }
  }
  while (++i < l);

  if (updated) {
    this._updateClassName();
  }
};
classListProto.remove = function () {
  var
      tokens = arguments
    , i = 0
    , l = tokens.length
    , token
    , updated = false
    , index
  ;
  do {
    token = tokens[i] + "";
    index = checkTokenAndGetIndex(this, token);
    while (index !== -1) {
      this.splice(index, 1);
      updated = true;
      index = checkTokenAndGetIndex(this, token);
    }
  }
  while (++i < l);

  if (updated) {
    this._updateClassName();
  }
};
classListProto.toggle = function (token, force) {
  token += "";

  var
      result = this.contains(token)
    , method = result ?
      force !== true && "remove"
    :
      force !== false && "add"
  ;

  if (method) {
    this[method](token);
  }

  if (force === true || force === false) {
    return force;
  } else {
    return !result;
  }
};
classListProto.toString = function () {
  return this.join("");
};

if (objCtr.defineProperty) {
  var classListPropDesc = {
      get: classListGetter
    , enumerable: true
    , configurable: true
  };
  try {
    objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
  } catch (ex) { // IE 8 doesn't support enumerable:true
    if (ex.number === -0x7FF5EC54) {
      classListPropDesc.enumerable = false;
      objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
    }
  }
} else if (objCtr[protoProp].__defineGetter__) {
  elemCtrProto.__defineGetter__(classListProp, classListGetter);
}

}(self));

} else {
// There is full or partial native classList support, so just check if we need
// to normalize the add/remove and toggle APIs.

(function () {
"use strict";

  var testElement = document.createElement("_");

  testElement.classList.add("c1", "c2");

  // Polyfill for IE 10/11 and Firefox <26, where classList.add and
  // classList.remove exist but support only one argument at a time.
  if (!testElement.classList.contains("c2")) {
    var createMethod = function(method) {
      var original = DOMTokenList.prototype[method];

      DOMTokenList.prototype[method] = function(token) {
        var i, len = arguments.length;

        for (i = 0; i < len; i++) {
          token = arguments[i];
          original.call(this, token);
        }
      };
    };
    createMethod('add');
    createMethod('remove');
  }

  testElement.classList.toggle("c3", false);

  // Polyfill for IE 10 and Firefox <24, where classList.toggle does not
  // support the second argument.
  if (testElement.classList.contains("c3")) {
    var _toggle = DOMTokenList.prototype.toggle;

    DOMTokenList.prototype.toggle = function(token, force) {
      if (1 in arguments && !this.contains(token) === !force) {
        return force;
      } else {
        return _toggle.call(this, token);
      }
    };

  }

  testElement = null;
}());

}

}

原生JavaScript

类似于jQuery库的使用方式,使用className通过正则来添加或者删除class。

addClass、removeClass、toggleClass、hasClass

function hasClass(obj, cls) { 
 return obj.className.match(new RegExp('(\s|^)' + cls + '(\s|$)')); 
} 
 
function addClass(obj, cls) { 
 if (!this.hasClass(obj, cls)) obj.className += " " + cls; 
} 
 
function removeClass(obj, cls) { 
 if (hasClass(obj, cls)) { 
 var reg = new RegExp('(\s|^)' + cls + '(\s|$)'); 
 obj.className = obj.className.replace(reg, ' '); 
 } 
} 
 
function toggleClass(obj,cls){ 
 if(hasClass(obj,cls)){ 
 removeClass(obj, cls); 
 }else{ 
 addClass(obj, cls); 
 } 
}
原文地址:https://www.cnblogs.com/libin-1/p/7712809.html